From f826c9acb80414f76925b7634236a956a84c3cbe Mon Sep 17 00:00:00 2001 From: Hari Nair Date: Wed, 30 Jul 2025 11:24:30 -0400 Subject: [PATCH 1/3] updates for v1.1.0 --- CHANGELOG.md | 15 + .../Terrasaur-2025.03.03-e1a0e14-src.tar.gz | Bin 125 -> 0 bytes .../Terrasaur-2025.03.03-e1a0e14_*.tar.gz | Bin 64 -> 0 bytes doc/index.rst | 2 +- doc/make.bat | 70 +-- doc/tools/ColorSpots.rst | 70 +++ doc/tools/CompareOBJ.rst | 18 +- doc/tools/PointCloudOverlap.rst | 38 ++ doc/tools/TriAx.rst | 43 ++ doc/tools/images/PointCloudOverlap_1.png | Bin 0 -> 177884 bytes doc/tools/images/PointCloudOverlap_2.png | Bin 0 -> 142435 bytes doc/tools/images/TriAx_X.png | Bin 0 -> 23576 bytes doc/tools/images/TriAx_Y.png | Bin 0 -> 24178 bytes doc/tools/images/TriAx_Z.png | Bin 0 -> 29482 bytes mkPackage.bash | 2 +- pom.xml | 48 +- .../AdjustShapeModelToOtherShapeModel.java | 74 ++-- .../terrasaur/apps/CreateSBMTStructure.java | 67 ++- src/main/java/terrasaur/apps/FacetInfo.java | 258 +++++++++++ .../apps/PointCloudFormatConverter.java | 4 +- .../terrasaur/apps/PointCloudOverlap.java | 417 ++++++++++++++++++ .../apps/RenderShapeFromSumFile.java | 8 +- src/main/java/terrasaur/apps/TriAx.java | 166 +++++++ .../java/terrasaur/apps/ValidateNormals.java | 33 +- src/main/java/terrasaur/apps/ValidateOBJ.java | 6 +- src/main/java/terrasaur/utils/CellInfo.java | 2 +- src/main/java/terrasaur/utils/ICQUtils.java | 111 +++++ .../java/terrasaur/utils/PolyDataUtil.java | 22 +- src/main/java/terrasaur/utils/SumFile.java | 323 ++++++++------ .../utils/tessellation/FibonacciSphere.java | 14 +- .../tessellation/StereographicProjection.java | 9 +- .../java/terrasaur/utils/SumFileTest.java | 69 ++- 32 files changed, 1588 insertions(+), 301 deletions(-) delete mode 100644 doc/dist/Terrasaur-2025.03.03-e1a0e14-src.tar.gz delete mode 100644 doc/dist/Terrasaur-2025.03.03-e1a0e14_*.tar.gz create mode 100644 doc/tools/ColorSpots.rst create mode 100644 doc/tools/PointCloudOverlap.rst create mode 100644 doc/tools/TriAx.rst create mode 100644 doc/tools/images/PointCloudOverlap_1.png create mode 100644 doc/tools/images/PointCloudOverlap_2.png create mode 100644 doc/tools/images/TriAx_X.png create mode 100644 doc/tools/images/TriAx_Y.png create mode 100644 doc/tools/images/TriAx_Z.png create mode 100644 src/main/java/terrasaur/apps/FacetInfo.java create mode 100644 src/main/java/terrasaur/apps/PointCloudOverlap.java create mode 100644 src/main/java/terrasaur/apps/TriAx.java create mode 100644 src/main/java/terrasaur/utils/ICQUtils.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f69c77b..17424e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/doc/dist/Terrasaur-2025.03.03-e1a0e14-src.tar.gz b/doc/dist/Terrasaur-2025.03.03-e1a0e14-src.tar.gz deleted file mode 100644 index fd5ddbaf6234f778e1dbf52cffe472f70b22b691..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125 zcmV-@0D}J?iwFSH-D zKTTWJ-C(9sH!B$|6`C|+aaeUcx{c&fOW!cZ|m8n`T00sa61wuCa diff --git a/doc/dist/Terrasaur-2025.03.03-e1a0e14_*.tar.gz b/doc/dist/Terrasaur-2025.03.03-e1a0e14_*.tar.gz deleted file mode 100644 index 0743b5d72206019fa8b6b15797687bc4ecfda427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmb2|=HNL0_-Hx`__. We have not tried using the softare on Microsoft Windows, but users may try the Linux package with the `Windows Subsystem for Linux `__. diff --git a/doc/make.bat b/doc/make.bat index 922152e..2119f51 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -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 diff --git a/doc/tools/ColorSpots.rst b/doc/tools/ColorSpots.rst new file mode 100644 index 0000000..15485b2 --- /dev/null +++ b/doc/tools/ColorSpots.rst @@ -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. + diff --git a/doc/tools/CompareOBJ.rst b/doc/tools/CompareOBJ.rst index a0ad06c..6e95d46 100644 --- a/doc/tools/CompareOBJ.rst +++ b/doc/tools/CompareOBJ.rst @@ -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`. +`ParaView `__. .. 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 diff --git a/doc/tools/PointCloudOverlap.rst b/doc/tools/PointCloudOverlap.rst new file mode 100644 index 0000000..e4f05a8 --- /dev/null +++ b/doc/tools/PointCloudOverlap.rst @@ -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 `__. + +.. 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. + diff --git a/doc/tools/TriAx.rst b/doc/tools/TriAx.rst new file mode 100644 index 0000000..9fba689 --- /dev/null +++ b/doc/tools/TriAx.rst @@ -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 \ No newline at end of file diff --git a/doc/tools/images/PointCloudOverlap_1.png b/doc/tools/images/PointCloudOverlap_1.png new file mode 100644 index 0000000000000000000000000000000000000000..376b0aec5d4221020129d40a3d591742a2bbedae GIT binary patch literal 177884 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

q*q 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

sZi_4a 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 t6?POg1H!m4Fb%2Uoq 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Ξ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 literal 0 HcmV?d00001 diff --git a/doc/tools/images/TriAx_Y.png b/doc/tools/images/TriAx_Y.png new file mode 100644 index 0000000000000000000000000000000000000000..cc12a94cae5ad3711f24d5bf6682beafebb6dac4 GIT binary patch literal 24178 zcmeFZhgVbE7cCsbD;A2V^yWpS7Xbz7DosE@x`ZaZ_a2&FMNpb_BqRu-h#?>)^r9dj z1QKfK0YOM0K!5;|{*J$IyuadmjAWcZ#yNZMb=F>M&b8*@-7`ZSrpw%yK_C#5?h{QD z5QzR82y~A3;yK_i_3F?jy{QbQ>oE(BZ{Cq*r90DCIJY2#u|I0K7fo_6y zH6NLU+pW)8r;6+65oij6>CWaTaBnoBEs_G?T!qhZqd{Kx7*Zk5pYI%XZpDqs4`rY5 ztvGRQ#ohV#l2QM{ZF{b*i`-7Pag#6anxr1C6UxvQu%817TUsBi`c$-UXGmC}&Z@#D zz6fSKmHlWQwX+OmU5i&gbfN>McU6Z)5;%Ys*y*@|!;^2=LLgdafU59?uq>_ zN%h!xZ`&J{U!|NkTm9|ZT4N5@R`!+$$%!tQ4LXp<#Uaq$ih+JVRrScgn7~u_(?7?> z%`a(lA@hZwW_S0kr8PB0B?Sv&x3!3YKD3HtFbUhSG38DEGEwDcXD?;n-hR4H)i=2h z(iD71|1JANRzZG2>xm;Q3nw6{saallvqDpTDvY7Q%Q#DUy^kn^7i|p?P8BFniHX|z zb!4F^*_4-pk z-YYBkv}4bakKDCXLFlrSwkMo^$}}?#aZv>3k@@$m8*XK_wE2kR)0R}IRg>A3!SPSC z9mPRvsv2cd9Tt^}_jPkpzd~4W@Ot&$D!cZIp8Z~8BJg6Fk2J9rDqgd!@{8FYkJx|GohBf(A@O1@8nYVCN)DE;p9q4KD!v;I3gJFN)TEF*}cQ`sAy15s&ZnA!& zQ#t>-&`L9@9j5ffZz#z}S(7-dFl!#g?R&W)x#O32c3f zKKvm5bHMONYwN-L1m)C@(_ghTLPS)k5^_9C!=3rdu;NI(%&VS_qEhmu7pg1eylXPU3NN+^q!kJ${Z#%~RIU+rPn+fPOzZk+ zcxzi)%T91@6+$%X=8KvE@X_I3w}N`a5fs>yqXn^hZn3A=FOPsy&eL&!<2(A}g=;_P zOEhY)hkG4vF9os6G;Gj)5!PtXCb~LS>J>=)W2%dZBbqW0&9_pbC7vG})s0wsNLy<> zPH@#oPZV2Ps#<2MT9lAr?7@=#S;yIC&TS7+SR#mN!MF8nbIB-{3X@gP*AJE&mkX9u zpb4*-cm=0OKNheJuAElX7S1Yy_DY_7Y`kxj18oaMPBXtqN*iq72Ikz*zKsRmDGA5R zsj(-1r;BcNp`ga=0LqHr-;T$ro$R*=w6^$qF;6|2Mg-X=_@=#U^33TBG6@dF-jP%< zN#UFu2hC;PKtS_KNEWDw=CilsoRV3${~F-{FoG#y1ZM9)8<#;s+rahO50V?y4j0}H zTRS`{DyoGX{@xyv-MD}Vie){-j%>M`njs=(gJXLSUPNk}-Z!$hv!_XLIo6@;XCu@O zpQ#;gHPvqRsvjJm6{6|C6*9Ja(0wy}jO@`7Q1MxD<$8}z#bP@ZtXv`NWSLy}YdmB0 zSy9lquS_%Y2A|@XuCr)!kMmSUZz?L}S1$&M+3%K@0gJVaDkPt-C=dehC-DVQr+%l$ zW$Gv0mZ*k*Ug`p%a~(6G9<{&PFdsqL2tpW0D(L$KO9j=gt1C5dbHFe?>3dFw%pXEL z$GSdWhJK2=$-z;j%;8h3ucPBB+B_?eG%V9$IUk^-Rnk9!T4t zPT3Dl)4i|ZsIL^11jowxPkA*#$^J z{oz5|PEOnGqR8}MNo|aft`|Z*>a0BB*@p{XS%4dtaR9hm%uxd&l6+w0<;$FBT>cTd z{B>CIfwD3b=f>(FZ9Fd$b!0Mgew(FG*g^abzL|_fR{qn>m|fJu{JGyGI6~JlUB1Um zuI}FDk&eSXvieEGS=xX1UY7qg$68w$Rvp-pKr}ODjo9Dym~cerkL5vM(-#v*M^R|; zJA(bn6!9)(@=&0)bbFdR$zy%|N%{@aq!yK_my+E6#62}Ek_ z?ssaz=_2NABlavY5mN$sCqg(LGaFg>yS)M4Xj1~Jw=<9&hgX3dk z2F%%3=Q0NB^-CeJYcF?#9YQXGyQSBawrs98P}p&*$mERmQi9X zd@km+n|k`4a!QRoAgUMMV7kdl$Nlg>8bR@lT+n`owN7=d1rX6pp8fd2Qm>Mbj&HFx zF(V^mD_N%JXnTC|p_i(E5M?H@A2Ij3lGIl~WiQcumk)<$GN^AmK)&9vWlOJ~`cIK~dFYHFQdeviB6?RuYFp zNJIB$bjky48B~Q<-=|Esv!%sR@ba5^KlS`gt()wfkM@!;jc&8tJijSy#4nwLT&^LY zwn^(|zk}tW(SJ^B3r=>Bv06zNuEv9yUVNKcT~|M`E_g$d5Jas;ROezs8Q=E}ovbzq z-@}A%?b(&CZTl?4XtK|UhL!*IzM1mSx2VdR;yXc zo7vMWtEx_?I{r`#j*qXduIEq15u>=h@|CpJl5(ApxR_DA=;r?SoyoJ@n~eX~fr zDwnU~;AHOICZbUE_T5gUj(y97urYb|-o{y8qj#SZrhG2gSLK1BI%t^FUDd>&jxy46 zyN=Z#^pawNL&XGh08klqvSHMIDlfzRP#kb}YXBRnAI}*HB5I3&zaXAu^b-3XdCWY6 zw4)V?RjSP%bD@IPMSMDE?lLi5N!ic7`mv2B@Otc48gIVQD<_l5r*Zn@jFPM}hXp5? zGbk&9E^{F4?2y7DNxzW1b=qQdx=HIeSv*j>XE{oBl=0iQbq*N)Ip`+mG_X2VKCc*D zx+{)^L$BuQt6f>f(WYp&8x`{C)Q3KLirlD+psp*m&(#rSv1^V;U72I-CriB2TgIF2*mDppB+@4 z?Fg$Q5cJ`rT5KOcDrV?Vx2a-cB}syHwP|1wg{KqUra!7)wUSr#wY|+}d3}!YW?_G) z{~j0>;w+;!{<~0ROE9ASTABp(UFJ;B(H7S$?^6!c^v~+bV`0A~^p6wAPifI=_LQCz z-DW)2xG8FG0@#Z8oJpx2`-@9eZJ%VgzuBFV=pV!YBfm?)jzHlpfz;LwW>`>7`5g1~ zzp9De|2ceq&x})U^x=c?*q5Z2CCez+m+}(4+3jvfJG}G~DCOU^dl!y2Ro=()32$>X zdqEYfA6&=?wtSmyki`5rsZ@p=B+2^?N}6lHb(|f|M+Mg#vbAUuPa#2G5UKJHQxF6( zWmWN_*Or~3A^YHAz>!bUM)|QsB=RA~kgmy0(lnByA-|^$EnW-BDSTx1Rs#~t6Kt8j z`F3Xcw;cAO^0>gzCUU0A&RcwsOwROx7@<0yIl6h$*Mni$?kap1b9%v2jZ z4RF8KzpX4cT-hxpniz5BiK&X0)X9KX@9x){h-DTz1x|jNyA__p3R`3-ejN66YFrjN zm^p$yg7xlSV)|XVJ<>c$90ehO8-Raz} zl75`^5%vMkrdSkgf3{TteQwUte?4(VjT~ZcaMrn}k8dN1UJx*OCjNvrQAkf&PLytE zobPCFJrFqSD`<5+=f99#UDbgFr5H%6cAV^ALG5{FRarz#7#JEF_V20`40guaUa>4G zbz|KoOsvJptSXMBp)J53s9+R-{`Ig&!l3ifYDYZ=)JZ*{M03Chl2RF`6> z!c$5E&my)LBN~5iBQgX|y5=K~MLdJKK`ei7E>Gb{IC26EFmLo{<78}_=xxq4xc>6* z@t)ivNMYTGr>b+cjJDrQodcteq*Kkbql^zohA_sS!hT|VUC>j5W}>(1pHdoTCr|X% zZXYtxNcGf5qY+cs;a2GA=qQ;SqTJiiu}A1Q!q^XE=vpuB?}Z6`+x}eY=JZrjHRsDX z2}pm%=|LVjyApy}MIzof#uRLKrW|1yQfWtx2}3T9&#{5~9lg^Ye$XFW$!_@C%!p8H1lkmV9}Z)GoNQa;2Fb`N((u+2t*ECGE() zKZx4Q52~cpk29W3w08~hexuiUd4a!lsD68WwcPufb1Z}zlV70z#jer!b3(#!_3D&9 z$(VjAx25eTwt#aSmfdF%xw1sI-VH&^Zt~-*o#2(gTl3ng;nEgOErtL;remtWzE4Q z_mVoiwwF`-b+@446fP-S z=ecHoHt8%I7~mC9MPM;_f-4iQmF1$95w{F1A=x3y@#2@QgBior`qve< z>y!)YSFUv!4vBsbwT==Zs8-D1Qob)Z zug8|bQ)2-!r%(JW^P;Uh49C=S8fS&_-?SL2KDpRm_Mt|M_pv5xvpGuEr%v7ho4I3} z_UGh_yo{S=Ql$$icDHLjHp=kr1%Bpp_qHCs**zwC1JuYX=SLaQgjD|1Rn80b6zs8? z!(QOxwg26$y<_e?4VEK;(JcQFtyi2@ShBQLl~-7x60w^a^M?VL<@Xmim>2#Hr)^L> zs0@Fjvxu}uDe*2=d!w^>m$&F`5P_3V_DxVs`t{Wd{kWOOUz4pog7&`$G~=`G4a{Gp z1>Z=y>D!vlI$|4JM)1jY(W<`Rs?8}@EwA0%=U8o1*-1<6URR!nP$ml6 zAbnSdznuWyx!C}OMp`UIA?8S#BFWxhS}-;e*O#U8cOV<1)@4AMuxf4BcB;cG&jpRP>kvuJvGYgvu<2vkr#tM0 zZ8YrBA>1aKCf@UNM&w&V^K2tR9^+a&6S~HlPsOHs6FG%{Bi8O@HhOOa-Ox_Kn8BhLfPsN_iAO9QTzMy8JempGj7yTvt z9TMWb9aL8Os~as&2Tej1;!#Y9V`eKIae(%f z6m4{e-M@rek(-Kzh5cyC8Z|AF@4YoHDS>jcsITU`{(RF{gey{@`kAQoTY)lkJ$V2N zuq`|W`M&f0PmE<1lB2!7@j&3T>}+YTdux!6H}%#ZGULr@N8i&;ff(dRI76cIBg?-Q zSYb6M{@IlXGmGb!kbf&ifqeU?1kKDuXg=navH-^|Oh)MZ>(J|38(Hxb2mRKniyQwh zqAFK4NTAgyGR~DXKFEqD4Ic2*5z(3;4-tjc594E>9X-7CEsaO&0o{XZ!inoFrz`PV z)yIpNl7!aq)2Et*n4|8Y4RE+LA<}Cu3jXdViSFKEgmfHQ@S5v>r7d6$#Ty(NRP8^) zQF53X17#hvRvy2M!X?Mw(QHA_N4gTjDLxKb#Y$isuKutoh#p$lxufA$!&Y!6UeNIh zQk&^suFDF5jMUB+#aWwLTd$>kOglU5IE0@4DL7s2UJfMTK`H0h+!qLc2xWfKD-DIz zz3ydiwb+BT#bsn7+&Zg&C+KrEwnn0u_(bNPdP7muJ33;BNei(V#vd)d-ybBD7fjJF ziQBhS!iEfe=47-;>3vBu%_=uOkml2nWGL_&9nJUvf34$|C=hiv3b&?vAa=>G0JJ%j z=vF2ThgnD)ukC{x2ZoFa^7Wxqst)1cZa}q+o+k5@y#p-cE*nP3vo5fX}7k|bd`1g_>2|__I~+Rc*p8F zmIBT2*ucP%amMHn+YTEa+{_M|1s=Qcny7x%opWx1Wc=#F=Bo^?{WhcdO1q-Wm|apu zle5T^3C|HCF*Ql1@CCbEvyZ8>2dW@M>h>xgDD`CQm44-Izn8Plv3WbH!|CWzUh(h| zfAe(DUD2#LlO?p@{5R-%U|Wyl_+iS1;n? zZe#%9BkdbaE&9kg;ArMcCLHHRjhOd{{a3>aMTn)$S!FM44V$zl%IQLwTWN74aV z(yfDjiGFh0ea#Eli}~+4sxJS3t#fv3e4mQx<55?7 z@?3;mHB<8TO!w_XqamUD2Soc_Y5OnuLZBHDvvp#0MvUEeJ5iwXC-#gLfp|@7BQ(5D zoR2h5rLYGWBw5QI_Pse+tgnK{COd+1ZJ3>G|ys@4o$=8Z#>$m}M*afdo+Kzwno)@r5Ony7VqjrHkwzsGeJ#xp1#lpk8_<4qRhUHC(s z|AcO*>y$NwIrS?zo{*>73!YJ1sGARFFjxA*p!GK%5){c%#y8MNEEZc?>5F4s-^N?x z)X$DFYleHuu}e-$g|BV_fjk9_I_4d1l@XDypb#d;X46 z`sshowc)L9vQD==o0I8b)2VtRMjXKeWv>#H+D4l@U;n6{4<%txur7=cL97rjgM$zk|6eUY!Lc;s6(-&)kM~BP{X)p*izhhUWnxXN(=+26S`J+kl`d=1 z1Bf~-J3KGGnG+TAyEsgh*u-Qk7J8u_J?y3QY53n*79*dnG)AcpHyJ;a_3zG}^ed4o zYVpl~KKAMGlCxQE<{M0Q+V;F^&pjYEyDsixr7e*^sZ60a z-Vd3qE`Lg+f8ZMbrv-G=936ppii;FeS5PG9heZAqi*8+4dalhG7v^4Wt=7cxP}AAD z-^dPE%~QJ(8oF>Fvh9${y_cStq`D|++52_q+pu1EEuH^e7b*b!m*OjDj5#+p*Vo4s z3y4~94hDTa!ckyNR)9%l`3a<_t5`U9veWo-{6%H{JG^x2n?DgjN^JHALQ(b`KbZ}1 zQYS9cuQ;rqTR*3?4o@q(p$4grUJ2W(pP4;4WbY;Oej5f*p-!1M;j+1eC?n26Ft~vu~|L^Ig z#Nw4?*5;z17bx+|GvmU|ELcVI?2SUz6lorZcuTI*HXiVoiS6-1C*}`PoLE8E;d>5F zZ`+B1U1F@;i?FcB=T2qMyf+VRMJkw%bdMw6C|~Bv(`H_{2+)!x6DV5wiJ0S^tO9EU zb!rO#Bc6Px1&5x#E)b!&5Zqd9jtC2Eyl=4Fu1e6f6{s7iIiw(DdNfmLf`aclItH6A zdHe>YTm`&d5H-kiB(5%Ou(=T}qQ$Y)@3(!za|fuJR{!&}vE;&^XO61RO?Df6I(i4< zJE=cd>O#X@_`uBbyX*PZxDWxLp<)9k;-^V-K`PveFPAid4f!M&-I52tQys--6|+g1 zBK=~qV^3NAW&uW$E^I%N@yZ`4#LG?B#b4(mvf>Sz`Z?{i*YTdWR>sDQ=NG}OO3tMD z10I1e3FZanr~BFTFLeA=m7}-U>$2g((&{YQMEq&87=_{*oC1ftXFPa02eN0r7w|zi zyaEyQC)($qj@9r=JHZ+?6=fT03oS-1%p~P-Gp8QOc8-=eBvUAqV zFZuE%YADe(7#L#6*X;ZCSfz4pcKK5lcvW1#GzG-hCD1+@6UBUX%+k=K+u{aPn^|s5 zgo!>*OfXraoUpoGKiBKH*uV=~ilU5$z2x6PFUA;!Z|w#eQ-w3p2+ZE z*2b-ei3zmD)}WZh4g9M6+E9!%!pE!}3}Ko%c!(olAQqdtUOjkJFG=LRW4cCFeeETq zm@S=oA6w3EuMEzz>2Ho(Y5sP)fl%FE-(cWlNPdNY00igVwlYX&8 zlqGH?pJ5&-$r6?BBAK6EJteG^Wud^d54P*}6&G!f1rI|MKvws3f zjP}{FCx>32$q-7U{@2%;_(gl1t1{nYl7GO*XSKW=p)JWPTIGAWUS#QCpFcTdyXf@= zW_gu#-BE74tIl2cV+}0aAUlzJS(dGLGt^Y;}s_D6^Xahc>)Z#jBqyhbaz1bOO=Ak+n zO+ezo&RSyjn<#{BD*CEr9Cfgagf0&*kB7jC!GOtnQ> z2&QTlAFtba!Ca=I05u5J+f_EhWv15n%?>rK*zHmon-UT|0*=?dAyAq{by$=q&K|ZH zOg+eqNBpOSL?TPp)n_*|rRG7<$FX6^AN$KycDD5ra<_xq|b1{11d5q*l%H4!hw@?}di!Lf5=Wziq? z+d3HNLU61nzI>Z~gRWNX8XY0EOB&|w*%NW}av#SMq*Az@l7OaH`{A1T#X&stu2UT| z2oSO8QXhjDGytM$Le+dUe{l%aX-x@i5_`WWlr(Y^`anz;BMYc#gsy0Ka@j(QT>NzSi}hP!HniT6!4a{qDT=RmX#F~Jo_h{z~DdM=02{dvVFUV>hk_kOiVYC zEg1Bg>i`;WwesiZ_G0H~Y1%mgP^ACq#3B7|;mn4?%q92H+vvA7ZM5&xqZu=!WSMMm z?D?Yl_K%xcwgS%0LtlPd3oj}vD;sdD!Wfx&dBldk!94c;!qXyxUounv^yJRW)<)=r zVinVhBmxdLynlBwO}HpH4FIlK1?Xh3tQ8=oiF#GEqa!yvC;r^ zW~;huZURK5J( z7ONX&y0L5?3#xi;Qsit}k&Q%>4LqhO>F|t9dK-&2s>QU8K1c`vNR4u3iKrjf2=8x| z@ri06hP8`LoS1ES+a;L}2GO3;KBNti%gP1PzS#$abSf%D6w-_qDN(wW2OxW)fw)FtCa1X9NwWD3~~e3W{nEm*0NZ0b2Y^WVBsxdqHW<=&9-z zc~zdaET{{$EqJ2Pe7Oul>H7VeJvi4_vQT0&VlP+BIh$%IlJ zsl!pulKoTYxYAVyjYsrhaUiDISDui{c6A!#xw*s5)>AVDoAGJdx?jho<+`40JH^JA z#N(8930I;rgU#ZRii5TxxOPE#OKF* zP3PeLsD&Qi!KFG3R&CM0vQS(j40iJo2ZO)x-`MtGCfL(#UMypzwhnGK7De2HRw);s zQ{N-;+TvN{TqO0H6`pml+SVcP)Vp&ro*(M|(NQ-06=AE%-%(Vf-Ht&R^bh%|yWIrv z*8k=Ne&xF{Pr4M}Kq*~r+1T&=63{0VdXdc|9ghh8QjvJOl38%nyXNPm*UUc6ZBf0B z!Cr5DHx@sgW177LI0Sc<@sP6$=KD1C*S@VOn?xtVB z6oNkuD>6?zi`R^jcv)cT{u>b!@{1zN?mUq*StpamMX$lE%^^~3g^BcyR{)g0NZ zZWx_5YkR$F)!E_^Aq^dtXI35wvVUvHH`IL7| z2s0oJM!fmG)SrII)hlgi^eC=LgAF?z?+oR`9&yv>&$;o+^_e2a{|39_S|_k!s5(?TV@c}@05r_kGtdt z0pOr%gU9duH-5m?Umvbhr7>Z_sF3WB1G(Pvrp;}NO20iyvhe@fivLZ{(zlOiA;`7C zu8Exj&qaCgT*dp-NY1WER<=Bo{oce0Jy^aCljMVU?M66SSx7}zTOgKY!!)ChdJJNK z;F|}xmw!}NRmHP&T}=;wSLHJqPbpLKuY7E(RXf=kM~-`%|CX12AE=qKVZ-!-0+{R7 zT)U_e(L%f-eYRp}v1za-n@Vn@;UfbCYxbBUAdr<4|HDbf$#JVUk%z$oh0iulG$L48 z12(UEv9cPkeP4xxnFH3X!nysBO$9H0=R0>gd`F|@{?v3IKkTVHCwwLPDwfYk zS?5o*k?J{#_giH}%Pie{22@#4Elw;u<(r=hoc z3->tY!7Vl=E&(9rqB!~omnReyQu#wthlZb76t>cS!Nxh|#)hh;Bdcz{|5=Ey3HTYp zhX`~Ma4nl|ZG3H@>-1;vJ1orZN8+`T&C>AStzj+E&ULL;^>%Q|FPjS>)y^ZW5Z0ZB!agv<}6o_xxx$Z&&?=Nh%{=8j~mGt1Xe@-3+3Jbx?AQp=T`F;4E^VZ5%AJZcsG?$rzx(ng1SSH)EmpL)oB z!VCpEp{$FdbH6yJB7z%M9;*_4~)*PkW3{XB@RWvX*MmX#zicq*V_3El^*|gY&%|2 zXS7be7X2B8CR`7-w%x$*DN`5cAb9VOh67J39Z-m`x^wW4EoOb=eDSZf>9p@-PN*Q&0cWlN24tq{C=2jvrriiyY-k_NRgfW zd3#V&X0_=(5PAR3nsxQ#rTOA0^OIj1e4aZU@9=>`3vVMme^j!!S!xM<-{a1IP#(gb zMP9x^zVIOh@Rzh)>N%ue9U?x$SK!WJXkD@gA*;oe3jf;4XZ&XudKP-WPO!p0<+ab} z)03Z5sO+;f8lX)LRcI)~9_|k-I%-2w?&ZQU9gJwFmA`y}@85^?y)IubN@g|81hA0| zbyTLcb28ae=J>p|9^~(6ZOqfcxL}C+7m)0fom#Z)h5!h(T~qiEh}R5W#h!RDWMXnd z9+Q;t+$&i|Be4NPT|6w^!b-CzW@L1aj~loq_0{1c73&DS2|;$PYpu9)%lg#Fs85ek zp|OQQ(D{l=0=wBsxz}-WrDxq232)ey*WUU05*Enu?&U0+EaqwUN4^4wr;uy|`g_Z6 z3*kVrXvv5c>Bf2G%*{#w%-fp7&jrQ9@hR&XDl2gMaMAK?o|t&xEf00+ajqS3p5}C_ zv&cxXD%9M#^QghDaz7)K1rOW0TXB6?b=I9=P3$_WurDKaPFLBr*`ap2qg*#270ReO zyr)rbeu;~Kwg%Q=OQ`+ly@1ngDv zBk{6Egi1tIyUlp-?lV0PRPykvVF;jn88{E_?IG(rMBwbC6=F_9O(>o2vvv0S`6 zgE~$??0v)eK{6|>%N(p5JHdz$hr~e>_O_@S&mSoDT>~qwL>_o8@z)P0K z5)wx5fYz@7flDa@j&t2B3N&~iJ`WBHu(Gj}G-9XHn;(b)EmF1k{<~fws z^gwqQsW(3$<{(=SA`BPW>HjKLMVE%*YJT12f4>I)sjRPKs^ zT)b-%%4A9csz}-Mr8^uaCkm%thg30$mb~xeMxfAw1Vta10@%7N8ILq~kaa`JYtIbb zS*!!ttFc~O(Vm_D#1gMsG9Q+%!0NbH!r`@_XYXQu7(vkswTHDLM@`1>6kY7ikS}&8 zYqnC;%Vo+KcHMmlPnNsw?QHGs=CBNKmnjLk+9~hqTpXa6lR(OwS4~6ilYr0wJo?vY z>O*90hwL3XS3j=|^x9accqJGZ5yECTGNC7JxbpfRRP+@tp&L8^ofa0Rh6p?f_WmN) z75sC&P*U0Y3`YWDU1M{*l|qQ$X?c_}vy7OgasbAFFqe5iF$PwFN z0=tH!%3!k<&UkM3<}ZMz45)hsP?-((m4msbTcBTq+zUwPwO?Oy6a{6GLQy_ z{ST3!$`YIi`ne&_w*328DBYq!z^=)wJm13quk3<+FA}AAX(LUT79gu@BXF{f{m7C&hJvS>u3*Owi zR8qVYyQifP&%bQ`E9iLb-yc5u8Z<4RGizz~%FU$0c%{vL%LC6W@scE&>!@1k=NwQj z9apPT?T~4PTZanS3YhVI25D*2{c)hsh(XQ-?)5jB!|iS3;Ma zeFa*d^okH{Qx*4{Za9@S7TB$8_yzgF$_RFsVC2dQof4GYd!DpfAq6vw-)?_@hTU zT|?%?D?&#U&vyHq++ZTZkba?jL&8iaxTMa+6{*uCZcb_TQ7F6oSz=j1JPWjm#jz-LDe399c1MZhJ|3?zABO%J#2Zlwl?pN1#)D|O0!W- zrE2^}R6;V-3t@o7-U@svSeNjp#)jQk9%;O`6*u*3{9tg)JEJj?7(P%sdt(o1ubIX? zt+BK#YA`f9H~@^ixcCL~;UV>c|M6d$rl6NkF0+9s`uPA-EUMio?3y0IZvzhx6PnN1pWRkQ&DxR!B(i8gsK{ARVa$5*;$bK~c%$V_J|_F7}A zc}%Shvm~2~ss-%Yw&#gN`~=IwCf{P!zc0h%MIYyYTZ*iE7a#n2!4tOdO`w3%TsmnM zp*|P$RB8 z|I?CjxM-=nSaMFVXGu_HhkfXCqB~EyJWC`|gNHo(!g{at4meY7s5-M2YbC7bVnqgt zySPBolSSKkMZn0F%nZU2(77Lwm`#&kwbGQweL{>G0uDUn>qie_c8LX~8#IC+SZFa) zc#&^A^UbAzp|CO0>$fX59$pxw@_DH*e72Xbj74YxD({mB2l9 zU;^yh>mJ+nm?rXG@Wy1O$$?nx>G9Ohq_ciiVuXp zhD_qUWY8nH2%{}(>3ZH#TVw1j9l9o2lq>@oqw4=|wMXIUwSe3snS*>|B=XkA#sAh+xu3lxt<-InEc!K*jJneM<%D3P9wr{fc63z<>j(0*W z^xRz4uE-)nYITF}QC!kZ8mQQV&qbeU1Z8z!Zzt(=@GFAlucpnfh@kKQ{JZdyJqHU? zeh|?9`Y1CaKI<{V9^nAPP*re=%YiN^H2$sXnQPNM?L-lvEa0jJ6_*rF>>jeMpGh(= z)0AhKnzuNUu2QPG{}OR;%KGNZzKwACU!LeZ96~#VhqnM5LD>9mFQGo(SK@sq*7c(W#D6jC6F8=FV3;p)N6vo$$T!Ab zuB1zeL$8~xM9ojeR2Xx3rZtclP6S$>8jv5w}jpfKFUl|p$5E6J=pq9oD`?Dp#honaX$bmP~HxSMHf-r8Vzu}&$>KI`5_7zb3d4M z)|s7M;O0$#X{|Oo`bO(pSRpe{zNcMWmP`6W*Sm3n&sC)tVMv1g$KvE+>cN0}#-J=B z*+iE7$GJ5gjn55L}NetsDjqJao{didc4()qXw&98vfUC75#qp~=_y1pn$ zZ#YrQ0gzobfoZL1L$CV%f8|feOPc zOIk9oi*KbTANpqBZLa3lwvlPeo~O@QjD#FzuTNh}lCg4?b$ZMVl%Y_;v;H2xo__yY zyvCe-I3F};m_5O%e(1K0x^LwQhSC~jzUM7yP@(W(AxJ?*?s zi5XXE@^CHCpH#)vJPU*m0#qQ#P<%P-M|XFw%iGy;AT~2AE9tnu%kF;@7l)3pO9=O} zP2&JQ8Q+L7e!IfT;NhaOM*tq5$}$0yyB%X*3X2G$Sv`u=;7H$$U970UHyF?urD>xs zqB6xU&&YXN52kQIas+pOLhpO`Y_hQqgQq@bm3+xYqVYYsQxc{CkJGTddww&eg}398 zFJJ6(1m;FBV_Pc?dvHI%uK4+~-48I77>Cw^Tazmi#CB8)>g=bg;zfB6ZfEr424P}5 z9K~NPG%bJ$HG`j(9W&0}0=!@CFVY%p5MsbmXLFyDSv`JTg`Cs!VpW0%#WYn+oN3~_ zU(AX0#gCvz^uz%@Gw`UrtdN{`oK)JgHu)ZBZ62Vv`MQ^5LclFxfq5-+_(6*_xm-HdfHgp%6M$!n ztMQy0Ctpmy=3+a(Ed!d)3Kz0rv!=>!{REl+0U!*lT?w6yno)5$*6F;5PUz%XhEP00dY(KWw z*w4>E@g|8=o(IZ0+|vLm$u7$4FB>ip>BTNv->~q?0vH!hcr95MH>gc4-86_!++|d` z4cr*JX0WPapPj{0;KxFj&o?&0Fhj53B7Tf9LzoM$0UfaWEt>zQo9p~*YFXD&+ya}f zY@`S%QlyuN9D+2ZNSD3=X(Aw9A+%84^uF0hH*`cmS|F4V2!aR_AQVFnsFVZ{ft?4C3llwhj1n2X$b|;ux@8u_1IP-;`Z;Agr)k@W%1|SI+`&xdX5O4CMP5A+K zgRNUaPd~9>2Hav5$_3Lgo$q2I{CIf!Uay#R)v&)-;LQ@+zWyGr+FeFN8Cr_`&HF`E zRLbw+gp?qgR|TCfFNS^tjwO_LItkxU9dtSFlES4am~4Fd)Zd0*@LBgHK0c)bC;zu( zvH&mhCZDbLh3DM9UyAjrWMcbpD|)eW-pflKZo655}#6j^Qu21ngb5 z8t|1JJx{Z~%@pk^<&?Zs;Q>7JXqMn-wV#WzkT#uH6+>P;<+lP2YF_A)20Hw{J5Zlb zt$&nb`Os_bCWo=j2td6ZvE}0OalE}yoN`&JkrH5~O3cINCXW(ZKO;gT!#l;_P4a?E zaU(e_4~|veOsYEV%pr?P27C+v45$GZAQQOFbgLG|&BrFD1S@X|sGbNae9YT@0%uce z(-u|~=0ZyZe<@eh`JhUKU#rv+O}{T_I-f3OX4@vWaynO&SCTT`Hvnko2qoJa0f*a- z^wWjl<@%R0g$#5_pJw>oM%xW?l(N2h1wq`z>3dG5QwUOeDyxwt5CG>12Q*sKt_bB6hmFde|d>&~(1HHiE=-oS( zg?;b3yl$={h)c`jDE_DSG%w!f&+l7*J5H7_qwO2~{qt7EbqxT<#C&$i%cS_<@9dSu zL!hcRpsnn(e$}L(kT$9Xjw6>m@P}ba6-TV1RlHD6{%| zyw;o}T_5klk4RP{Mh{>0SYmO@w2J0pLi>z##a-;XnAFHbWPZc86~P;wz3L&z*NldOIU)((E!+bt2Ys4#69 zCS%CDe;gMeG~AeIc!vQ4maq%t?Z6+l@v&9 zyy5bz!6`#`RG>k%lB|e#J(Ifij_MLlD{Gtd`45LZc!Z#YL+kAbXEa=5JBYDGi*nXjrA=t){_~d||)RhyxvtsY0U@Ax! z=875FhK@vkI4PRjHS6gAthciBE->oLE3Tm6d*$;A9SqDt{tZWW8n2` z$C!#>mlgRH3+$-8-uC7xhA;ot<8Ja_Z7kK(CZk5hmwg^^@ z%6D=UkF}4NyEjByZm#sm?W$A;-FClA(*t&-A@@A&Fyo5y_rpK0^>7G3OoyZ!cA8K^ z%L@yN@uYjsglIF^VjXRwIx$UxaB)hU(|I0cwQ&LzTL8x^PibEvoUi(4ySE z=tW}dQA=nH&8y$W*|hYeUUk1r)We!W=&o6Q+>;0NpygK&@(}EPaHNs6$iY1NTs%4= znWXg8*dpiX8=alr>5lmhu}2atdAA-22=Z7}__H7^1qy#0)!3)>Mm?>IG7>|KrzVF< z{?3&>_{1L`G}EB6ukmxwAmO&4U5e13b(Gj<2}u4v9yKGBXfC175Wo*W=_7M6sqjqX zv~$E(Phb-+KG0#(iiq$Ta=E2FKirb`=zx`?>y~aCP1tEk;#D_MBE${nrG({b^+x|8 zFW)rdNjqk?QJ$#h`eTaLNIsFNj)`G$ld;iHxd~wi5C`6$I6`1q)no~t50*67LAgPh zx&h6AsT?R$vuX%%6d&gY`GZRcdvy%RW6Z+oV*%a|5iu}WU{usJ?7+=jQdf=NLP|iJ8;C8xZBb>t zQ1`ql(H(StGH@Wgpu9&ut?JvLbc{fvTX?p@Y4fYlo9@!dwr6wGK@Y-2L@Wq*I-H+4 z7v5c_&5093;x@b1290QEbKwQE)lI8i(hzc#QxIqf5CuVR-cOfPgS>0iE9Sgr?*=tX zWIxF{L~&n-m&9fBL3G}4p-^Dke8b+Qk)qfGvggfgC!AU#+@AELgsvJ5t`}{rCcEjffzZ!LUO|ywk4H@o|U;g+eDs`WO5IUyTP#lVq#_SA4v%Ap58iHJ^hcB-eP7j{Zo=+1rx!a zsNu*ZCUy6%V5{TZmsQ*_M%~?qCaWP03z>H~;g!2r`UHNNq8pi>`Dp@~Y}qZU^y3l( z--3Qea=gX*#H|1l`XDN|OhsH$S4GXfPLmvnQwLJ2aovwhg0kwIlsomULvL1qW4Qo1 z1PJ~K6mX@~^r#Y5CGVymyC|GIG)v4h8W(fsv%ghj=rlG-BKtpF_+8uLxXbg)#_-|2 z&Re-KAi)UD8N2VDk4hgh4p{8~zLHSfB$CG)xg3M)T9Sf;X0%kn{bj~?$Y>({6- z%eK?g(o@T8Kjv3NbYx-ZD&A?L)t=dp=>Ese6*07|xrD!FT%Y!9OplIPWQ>cnsWI>Z zIdTPG^|L;4JzMZ0?4^m5wvegP1_qH zf%@74$-RU2$ryK41o{X#ji8nh-ko>TdZB|P8LarImbPcXQEpQuYsTJIr<0St(>{eyN7wV^dNW67tairc70F$e=Muq(S^qO1q_VkgO$d{e?A~&=xa^EF+s|AGDkxgWwiqUi4*#ag*nV{5@5hN zD1RBdkd{(FiHdJ-+_GfqHsq_EowR$11L-YbyZK{Pv~TO?E7~JXmt;j2zk12LqAZf+ zQu91wzgq3+S}D_ao#r)^+)Bri50~)rEj+=QPY_#%-6cM*4MD|(I1;N1V?>w)!=5;^ z;sO!FsyV)Ku%UEwB7Vv0GF&hRYQ9~>MqTED7uBM8Y6Hyt(|G+VSK!>`apQ;=TR%nr zI6GQ7T2tf9ujdbSfiJc&IXXn(4n}Ra>ssw1=X$t7I`f-ste}g^5t@-XP0^Af6;`e4 zDE>xO?lkcs=Cn$MZCtCwG6mmMCCTbiZ~wj>B{#)K**M4pi!UxkB{t?( ztg7hQy=h1(G-;lmX!r((+C~OwyjN0TndIve(&=%$p!E;pMvv*#q9V#BRc=sp^X1`V z!_*8Bk#E>SJIEV}#|`@%3D%R$wCCPtmD0jg>6~YyZ9Bm)uvAY{2_n=CDz3sXCk#ww z^#!Si(qg$c)lg5`pc0#Nhu70(b<=!{5?A}3y%bzmE?_8Tb!(pl>#uY;fgeyi(mo5> zWfKwMeZV3`(~G^_96I(_o9{nRv-G^o7TnZ_+=`w6fyYm*(lvOva43CSXr}D_A(?3x z_yJdCq?IsV*YSA(dHKm2YMUYR9H$Ci4Zi-MU#!vcs%n`p=LhEe-vhNxEV<%A*%zg< zIn9&nG^`EH&4s0YOeFR0Uol5|3rlfcEKg1vzLW4)%!liz^SF+)s9`&v3l5 zO6Y=XjKc0MJCX}m#>bs9HA|Byqd#M2A~sx)>PyeTjB{5m^3UBPzJhvsIW79$ogF8& zeN&Ig@K{6Edvo`|)U!@0CxOp_eDHX!_uk{+^}@X%{LX7|mvi|1QTf*_<$F~!Mg&&h z-eLKhv20393)u+YwyNncGG;qu{1``Xfx*z`hg%^dw@=U#|Jwaz*5-LDgT^f@z43xI z`rRZ$HMlw9}aQId62hLZPEiIX3Qvu>1E1 z(NksR*dUDA@&e}LO;E9c7Q^DhnYnxmscwvcWH1E&wV26a%v$dGzus8GPDXD-6o7_J z^d@t3$$hBla4)8)4TFN_0)3+CzA|_UdXF7oW?STDJ%AV0N zVco6G?-kXr2gu7cMu%s&^uuz%F#2S~%+F19=SG4ps%Kf0t;hRLha9AN>q)Git`>vZ z19#(^PcT^ZK|-+o{7iAPB5aA&(2OXVZCqpfEJWB)+_y~ja2W!u9B=hDt{Hv~+o@l& zhQv6?Cwy%zEIg1&E|!5`Aza99|`HP)9YUiK31w_mV)d;Q^-oQ#aI zMs~@&*Bzt#&rFRxMT%Z7a877NrSnZ+%`6=P=+&Q$U}^%a<+|YK+FTP_>J41e(hRV= zTU(RbfoxAuJx_2@jN*QM75A7Ho%vbWS)w9=4BrhlBkI=3bi1GX>d8ldzfB}2QP@*A zbIZ{dc`EzwviChITh!#`>g?GcUA@TP8M`*Pi=I$R1dhc}{x+pPNLcx$ zw>H-Jop+=+0L>s;3aiA{n^!AiDTPI(M~f0H5{B6MM-Vbbl16rUY`q@gh5+YL61$Ji zzGl+Es=(Y-0@}V~fv>e|IWpEOW!|nHmZM4Lx?^VLyWQu_+?!-*?(_0kC%Yp{prWT; zwtF~GT^b>SJJ8Gf1B(db9Pymyi}kuP0V;O+yIsNz8v^ojVG55CyRwJ`;$IGk)#$zVc7R6Bj<=|HuC9k zl7IARN=IMiD|M}^~ zp3se{RWJ?{?Q#BP_q#PCabtt5b_z#kT?ECxExL4=g59tH!#jQDs-crUA&#Lt4rKbr z4(D)+75)V{$rQe8zQ<=j6=Iqon!U`8Zlp^0>Z|&Ees`0&yI$( zO2dCfXudlE#Ecahzpr+zI3hhff6frk*W+SBZShhdP{BjNBQ1{WVgBvj0Gy2LG%gT) zg6s1@puVcDs}ppOrvDUQ1yE${9CZ=3@(WMK@LzC?J-Gy(C)vtg0L2>MCCX?CK%vVP zP|;lufLj5I|2L?)&42ES|KA(h82Lf5oAqV@Z2QFDnrV6VoV;cMffj9|D!}Af;$skK zu6^q&+)lpU9*~gEsWaCq1|C(2qNI#32?2tjbf`j(c}v(GbqZFfJ_LxyH8iTT zO|T9UZCUkdgJ`C)N6tc%KZXjUJdP%mTZO~UO;#cf^$!jt>NA1w)N-ee$9V7*D}kX6 zuacv1NLmazBPi&ADtKm!=fbBRO?BNn_}*f6`ib%E=pZKB;$(7FGkjy-%|{RzIxYwB zOH)bp;I$fI`krj)@{2`F{bt7YGF3lyHx1khuLtN1Z-NxO1Efr z(`ZjvI0ndI94*Qu>m>*5~ev76NOd64=@8SR+AJ!7mSOSbCDfxGAQ)8$jD!X)=8?CAH+p_#}4_T|5 z;E%Mojie7^se;cs7O}k#9SL>8jxlNqDk_*0 zX8_Tc5LqkHq4*+-gX+t(4`Kmfiwzwsr~Q-DV>9@FC1;F!Pzxw{a{&UWR7izH*&p0YOLBM68YpTwrM$Js{8%YOE)~ zUz()fasUglr^0(rnU5cx>_1p~+Pk`d-deg_zO%RSN&cFw4+6aesmROdeKOl$U5;m5 z%%MAnixMUnUX(!&Ar$AHS}=i#Y^otccLS3*Mx5`G8#b#t8=#n+7Vr5iepg?|-~Pdr zCgVSvj?uBnB+N^pc;bj1<|EiWr)<0eGh-hO0Yc(viDX~M@>-j$0i z9UuSp?OTU_&_-Ifh|=wb^gS5{a8wY;+j{!(=pazdR}k*w`UiwY`MCC=JrQ_ZKci#V zJ+2^3tcm}vcmzMt9)F>J`3C)Q6?yad+2i{0^Z(o6|LNfW*}?zMe4)qH2sqKwzd+Yu z?uF&7eXWqrP77DpI^pA#Ow2DA+Z#&LQFItV1fRN&t{|vDSklyKC_)7mqecZmWDq6j zsMZt8zAM%`go}%}`FMZybN{2#9BER< zEKC+k~H$?p%ZaZ7$PE@Y+f}{5>(q?Yz9Sg(VcDf$9J_{iDerO95N+YfMkq)t@x9 zx6uuyeJxBT%7k>f61AE8Zgw||$xH00(YDiT8lNytkWc(rAKJ6}4X zmdMq~l_?4O!zK6m+2>_smU-~qs!ta7+6&QgeV zYc@i#*V;N!v`-p9#d{hZo7`gOYcU<2VV||2yTxgPs2%R0YvjY3 z>jR|a&xr4Opg>IUa5q*u}Vu#O*m}ol*v^I5{aJ(|VCJ z`n+PC(zY3$aTncJ;Tit7EYf!bdwmRTek~Am?`!26XQc$m|Hd8fgS1Cvs_-{;D>JpIlVShgO8W9}! z6*v|tU;zubi|=BSH9UnB?CASD>;3y%O!VzFp@#Zc+L@5$CYk1#anW}fjX!RS991Xv zqkS_o)J9;2Ma0flsQP9z|MNLVc~nwv&~Rsz69bZ%?-L?|v?lYpBR%PCSbGm|`^P z)#|^RO^0Y{#<5Pj4t+a+5>hZbALf81LjnwaIi7Az7Y3wR1uBI8rR|U4eLNepF;e7{O?O zdw}(Lsp?d#PQ#ZuJkSCd$YW|q%8Uec1Sfqtg^9Eky4^p7o0iL|(Pvg$T85T+%%hw< zxfFUhOelRSClZ1Vv1HKDOhfOz5x`jGIOoQt(UL_!5_ zX8(>!N)|3_n2n;zo|mR(I=0EQN7Oj`0xiw5XnID58eRNUrl&sZ()&j5+Z*$Ht%uVG z-z)cbZRDVyACG##&|3Snr6hfMemn`8GkS*tZ{~Xhx-hHiy4rXOZj3?E-8P@q6MgMC zBr&f&H`rp>e$)Ol3ia)E^wd@$T$n0r=3~Z!QwN2w=$&opzKg8C_W^av#ZmxMk2=)L zj!kXqjMqr{c>P9;w%hHY`+@EOU%(60vz-UHyU^}&fq(a1r>$15#BWH(Uhr>4#K1~+ zU8!W2VDZ_ufa0Jj6%Cl;+tF7Aq){>>A|NzXR)=pd4?G^ey-Rd9rI6WNqv#y&VpPAX z&aT+RoD(zIT8xwqxLxeNo|nG5OhRCQYFJC&p!bAJ1bH6azyoh4_iU3?g7RbcO&B4F z#R%}L46}CD9+H9j4aVaoA1ZA6L>y|&_Lo?Z%KssKo7nA{BNIp2!(Q$pJ}bU(i_`$) zTI-T*x4`nx+vMd152#6(Swi1eL+6zpr{q1aPT0mK`g5|YrTy=aNtn_%{fLLX?tAQC zfD_>Wo7RID_3b8$==Tj_fVNwdd^aPY;bux`AlBXE;kKHP1( zHZ_*%@J?X#v|M^4e;Yg|NCYT$%W2L$IUIQ!Gcxt`wkH^|IKSq1@~>Lj_g?yTC^wIn z5)>XziU%T}+fcf=?>!3o=qXaE0vl9Pk|Ag)`LkpYS3mkIv?w*BXdPAjJmZ||!xHE3 zEjBMcG%>2No2c0#BM#LbUbB>n`iHF3DV&2QFBP3;2T8+Mp*q*QZSLUI*Wb9z$M3gt zZz)tKK|HUI?p*J@r};o2=STfwcAwr~bYGlX|22RjHVO)m)>dsV(+$PE>6^>%Gee$2 zzawWQVM(%B_E>vk1Vy3F_m+W`|H*C+!}^}u`Kx@9a-KFzNl5xD*=*>FeJl)O_!lx{ zU5As_uTaQ>4!?b)3K>bOCnKBh2O@8q95yBh#eJ~&GCy<2Ni!Yg^cu>!*HYDp z5ctNIfVk;`^H8+JUZY)Jr#Oovxeou-aDr^9x})akM8R=X$r%}j#?{GF11?$R*0c_= zBvoWV-zx^TcLjGnEcwm3UDxN+2GaMNSKabTm>?Vwi5+NX@lmZC@Q0n=yH{n(u%cZj zP21`E$@haKKl9u~w{9RN^PaKbR%bhHnRpTh*wEsZqoWs9Wi_}L=X9TR*r0-KVGlnn z`KLglI&;#LLj$UC?s{!;y?M?5WDi01S^@n{_XIjFsMGg$XU#PbmC7oiYv`w0KCZ|s zNp+^MbG@I{M9HIDJL**B_|-m*p+WR}75NRv2!ev)PTPHP0xffzj?+fz@XycmF{Cox zyAJ^~9B+?BUN?Hj^gDPG5A@jw-wvHV+`(^|C=1YjaNDheKGU`++Lmd;^j3dc>iVQk zl^d2^U3>qXgC80>Yq}m#yJ0Hy=~PA)wmErVr+Dq`7LTIb3m-UyywbhnUVno~2A%;C zjqq_Z02L=xnpPKoLlIV;jhU8q zSNj~Rn7JO8V+E-^V`$o*7q9d@`*aa!gkiO_>U2T!$b=qYfd;Es?4;edXP@I96?0&- zoD=KhH-0W=D_CUvYFUdTR9Pv8ze8QkW4(+?0zOjtC9_RNRfW}&S}QFg=0Y`8b0vLkp{&;1@hU81zPbM|q4G^$y?f_m5z|t(;s*hcX9TSs$nMpAy=5V? zwPDxkeUWV+ah{e|e07z}@2V-2=#0SbO>E)f{~98f%d;Hw&#)IlWk1NFEqy$S;Bt1H zGZJ3YOZh~Uo~$E}hfhp$omW5Q;JNg|`9}dd$mr2lTT0`{XUzSW8K9{9`!y#aSDKvC zrqTVGe)`al*y&Wf@1#9vWxnWOz4*1{r$F&FQiaDJe(@?Puad-HR7WX7iCM0gb8 z7v?OHq2IIBGpf`jvxdM^Kd*5#R9MBFql6~NaY39vFeyP55`G6L>FWqO$$*d9;_yO5 znKvTY#!m>H;il`z7s3K1_!??AS?Yu(!&tI#oedQfuIlbO5-qO6T;hGxuK3UfJ(y$M zKeRnf_uLGy^%xD*DlV&Ct~v?Y(OV+V0lsvYLlbm)xB-LY=9;hQnbLq>h5_jmE*^f- z!#^MR!w|HahJTgDkMS>KWgh9Mut{1&H|95ealC=j(i zElekUxd@Xs=a_GUqJi9j0Qh5ky%#koeS3cAAF-XQU8o)o93X2)ofNrvv0>#wNv5$b zN=3J9V>ePhL!6i604qHMab_}@L*1TuPSb|yUoF;{^Ch&)o};}FR+zCLok)(F1a7L6 z?m`n^`qRa+R?syop8#m4=GqhObNf&sqA$e$e3a*#W?cO4a*nP(G9xNw4#Fa-H4uVk z`y&1|RrWZtgpcTVR%+d!Y!G&+N61 zM#kaAkB^VL=S;H<-aKptpZf!=K9E$`T$7-KeBRpo;XQd+>*ODyCsm!UAJY4BL}tsr zpFq&GAxEX1tc57D|KVwA9Cz%toXq?%Fzk|A!%f+o=sA>TaLLKv03cLn{fZ+A1tnZ~7Xrzgj~-MRnMF)B1lgQ`5^n$f^r6B-DEW=BuZG`e<_&?J5O@Xl)| z)hEAsNt9@DUwH=OG;@NsJO8htSBHsA8?RikeKak z$yJmVTxkl?rsMh&i*GNSox+6*d>SHy>{%L0TyScLSrr%?w&Q~B1XsP(G7a~ZzQ|-+ z)2Vv1N*Di)s6THz+j9h$%MHN2EeP z{wkcMWNcjdIk?Iw)`rb?djRuSs8(34-Q+TD3K>$L*K(DpvXmKeoxARggmq#SU@Ih6>lbew|>-_s{z}V*YFcc zo?=$iDk5T7S7D{8$7A7#338INbISTdY`eobppEvQ{uNs9C7bjW#kmx$uCh+MOJ?v? zCSgXkdj8mB){mvGis!4B!;8NtDI}2n1eqCHE$L1jnve$ox%>Y1$#ezi^B2Hv%S&~b zRqUFyV5Lawzc*d}bqsuc`XXla>3Ihq%7?>y)w72Kj^I&usWW?BOi@yENrsr!9>1se z)@KDJb(h(^Pi8|)N+oSmz;^2OPe7~~cAwk1g!NUInq=mXhN`WAZWt;(H(D?i_%iC+ z?Rz@}V$}(oW=$+h$zjvC=u4?7 zs3s7s5_z~S#I~uPnfA6yK6kk>**a<1owCrBj}sSZMP<`2I}jCkP%Qnjv$g{PbVN%5 z0D98*>poMc?9LJ?NraUWmn2mxg^?;!Ku75vap`Lv*+`q|WGI}yUt`V#<8#J;o|kesB) zE_6cdW$Uj#j1$Utu{6%Dl>N&t%iQYs!Va^emh~g9x_D*oKfdF6k-muYF|q6W6yxgU zS5}9W-cls(&_Qb}c;_Y7_dK*mI^eT7;-AHJ?tQNkvm}D6Oa&Gj)|Q{C%vjh!*G5m4 zspfguTq5zT_WVbm&T_^N(-b&D>dMGMIe%9D&m^3gAt~?SD)|25anqO0 z@p7OAuL#WPU=$`@4)xJ;F`~>uTQ^8kKa8F}v?O+;-^ydsZ#JWGu4474OPLhYa&<#B zX@vHyBdLSpr+a@3fFg*9w_elK5FHquq!2B2?;di%> z|8O5MXhU}nMH;@-aTefQkp$MKd;)xX8P?z|5%5vR&z?(CSkcy<72pk4UO^e@ zv!lnL0r2w0;eqz=d=x8eZ<6dkRvFiSv*m5>Jo7uh_$hl@#*zCx;lAb_iH=%%hmh*X z6z3GbK3Se_Ke=gUUer#C|KOvjkJJkTV0eDsxfGz9dOUpQhaG%rHs|Hnbwdfx)BNm} zeac~$dOjjQIx{EwMwib0Z_AE_hYcG+#?{URyTU8>>cq>K--raU^-Qg~0d^3FY~-!6 zG*VRP1M5Ro*5!7y8Yv!z4-TtU0FA$SKRsUz`& z$IIIf1$#ikt9_@@dN}`_)B@bXwoliCy6W9Y+3f;z5`?p$o_&m3aabL<1*cD$H3=$E zKb6@!)_f3-%FI%aq~4tnVmJO!Kb5nt=g-W*FliErD7-f5CMQgIiEKcxj~knyRhR#j znKGqal6Zwz2?;bn-ay&pV2GHz(uWPYz-p8ZsD)LW0%f(vRG8kR|HvYAl7a+w&$7g6Lv1~?`?|zPVC(-N9tZ{_MfHnoX8?c zp6+(1?&!qn)z9otdqaKA5D5+we2~lYa(sHwf+*%qQukf++A!qwWEV)QFAi5dqQ|Wh zAQ`MWeziCnv!dS^OeMu%@+0S_F~wq^^p3VUPAj+4&$VhFHrle!H6lW%f3l9_vYY-8 zR7wz<-MKdSFw81Pl#qO~$+)JJ10%Ed6T07tfXnd<^f+&wKmQLTzHR~B7+r@?jrj{- zb2C$|i}Ps2yrViy_CI=s+TConSmw-DHSP$e>7mI6Jt`Mbw-KC%EhkfdTG?5yFT^?v zu{=9~z1g)1k15!0Y<_y^zTAK-ggFq ziGnICwBht0rBVIdQe7td(mQswy@|U9yoP0nTVd9c+Cc_3=7Z=wM0eEb!CGJrqGsQ( zirfraT9%r%YGuryu0P>r(2Bcw*om+Mz83cQwXAt_N#DEK#Q0N_!P7A(FhxQ6Fbo2u zVk~pE3Z+JM+d4NhHt;(Wy$U((NuHb*rusA4nRtRug$mj|@Rj%q^qd3QK!**z7vea_ z4K1PRnyAC-e9r|0?9gN=H;Vw^N2))o#s;!cPIh260$`$MwTLYz-^;}Oi zS)!(!Pm$8G=i@f-Npcs;Dt_>6?=_;CuD7h0Ol@ALtPkto&nlflIb~;46KdDu1-f@U z!0w0Ot8fqUU(Y~I)BqpWJcwy7EhUz{J>CGYcMZgy!Z~)}xr(XhNjK$UWW4q7qC;Q9 z!Qed%{Xt^2=^oAxpGv|an^G+qvgW}IHD!FUG;I~224u^Cj)iN2v`vmAY@ETbp% z-bLJ>M;u86-v0QA3tCVCNUoJyjpBG^HqMQTNn2y^YcXs|J+~B;VL;~nk(w8$Wi~ZFv;C{D+IS8`_A>A0l+Gxr`SwncszqBE5d$ZJ z=z06xb|_GL)|l-vc(Q%_$@5>%`O0hE&>~qH%7P`d6-q$N{f@saUX|1Jr56V*{FUiA zlFpe5HZP0NXHOXuEavQMteD5PrG?IHiuqV3%d|d6VO$t+n;C1w@2!fJuk)zOk29)| zWfBX^InW!hny}b6R=Z^`KQG{sJh(C=W|g^;(RN5D6dgsH&@6p(xTrS|Fmuxf zcyK%t8qk7lzuTUXkBeTQ@mdw}D)hF485yGt^|E0N`>xYAsFz^Rxl@kE`IpY>tlDgD zx%^OJ@jY0u3O(}e)Gmk4p<~U=;XN2I?{mV253FJ-<#HdQT|XQ5_&*qTuQuR-oJwE* zyZ?iEL`Pepxb_eXvt-nMeTZf)LQaIb+?1a^8Y&v0Cd;#U+Xy3 zzWUK?Ngm#%Wo0-&dyip$h#C!3Dp&c-uZ)8U9Bo%!5r2MM*3!6I03) z8~ngVG!6eHeP(okHJlr^&4W~owAR~}nsd5p<2@MHPrL3)t5P5^^j5IV%8QySViPD> zN-=#7@<#Xjf#$96z0&5rwWfY0R8_6dm0A0q<7PKn$VRzfcL2)GNGhy779b?ij zDU}qN{?x0?hqN$k(ZfYp*%!yKDVzl%e?mJQ{nYri6gd=Iu8 zy!B19W|2)^=cq@Iy6t|rzhddW%|}v#YGRiHcAoU?)NRZGl>NYohyAG>;K}7(#LnhH zLMhw&{k5JM>NYty>BlaN;%$o*=a4ykvAu7*M5MjWZ9S5~#}qs;CZG>~tMyS}*|#hxfo^T$9Kan}zUGKv5e~T_tK) zi$HsQPH0HVeUAUt8${XGm0-0ueg#VECwgU5rGt2x%eYPZ%|>EO<=JiIi?a*$l>Ly# zgMQ)NVj@-7@ls;WLU|xJb8)+5eD;|zV-9f*XhqsqwRmwSTU$N0s0w&6_}ab?Vi!a!G~PiBA$rtWy!Bv~>tu z>a3%sYcu`qGW9^|%3WR@74BOU&HaM^g)j1_pgN1^xr1PN4Qv8(^ss&u?8DdFdj}E+ znuZpb0UH{@lmeXc@+vCz6;s)-_<}25L(@j|XER!TbD?P7I zN=*|*za=`L_#AjOMJv0dVxHQ|Y?)3%1BT$fN0!q%ARZ~8I-~jbr*}^twUvh0)^bDB z4I0M?zy8*8UD3=f?QhmKzZk9%Zl5|btbb9Nw(L-F*sF;i?44IDGF^ zli^Nv?}}m8rlFT=K$bkrEIh9c9~({3v1ZgFo@LcWfJT;s40Ef6%(}3QLHtt3KO2)DKF0|bj^E@EF2XTT(3_| zHq+p9={?_8SXuwRmQi+-T4#C+!qAR4op`mD;B0-@;$DrP^Kn0Ryz_0nEorkVYoG_e zn%cMmUEpOQDgd-V3uFgQg0Q4&=?d|GJ1ErV)`+7=+y3&p6?X3~dZ&mV4L@sTvgzz% z%JXY5xaHdEudhyN!0HqqZAO3cl>DZOeQr;7#cW>K)__eeHrLI#^gRpF1;tRgv&Vrb zQ(=iYK9LWFN`Y!2F>8JcC8s2E%S;V&R9eDwY`Lv62H`=No5RyR@bK}+VcaD@H01t1G+WTi54DWPW|z(ANBp%J8WoEC z31jP5%UFpsavb5?cR#y8Z+atk%BU5UA%=HJej#H%KA3EQvvQNqX z8igcJ#eC>Q_-hwoh}W{{PLK58!P$7${o>HldWdMX zR7IBe`pF4YR9iTCbNOno0v($r|7e~~LpPkqidIFVNxKG|6IFQ8^ra-^C=JaPrt3NvGZE>$%I2EB5o} zFdo2wkNmZXpVL!63w6|62*{cg+7-n;Oekm&s%EG+5PE)Vb7HZy3HI+kTfMly zh2LvD!UZ0n9nLNPll8m4lM$mJ)S$}s=B7w=a&rD%^mYHQMy~;rcP|7D-6Wl{W$bnQ ze7z)}?}H1_jIUS@w!u;v7|Z$swBJe3@YUL8MKBK=TPLONaM*Y@ww{Dj{>DGS*sjw`*ZW|RWC9CzdH@9qfL4;0 zBR-vzOj_duC9_I%+~lf#Wh0mwyj`D^bmhM2iEAq})%^uWlA6_KY=pxGV$Z;kL#Na_ zbH66=4VYA7#q}VDlSfaAsy&0-)F^!8t`I{4!Oye3LexV{zOVl7~y zk;^J$f1cGVjL2Q@pBWB?uA9O~jp%k3W3HTB?gco>)AA2;05jwMXABRl4nhNwKLz~N zxER<9kEGejPf9|f#Bge7Z7Cs%m4|3Hh)_C5(~y5?_8%U)3{9JBh(x}9Ogq9?m9>&Y z+jr6ki_gO(lfetgSLO7gp77Ev9S3XGm4-6n#S^FEwY5#qL|S4WrZ^)}){;lc_1A_# zbW~XnN6Gz&&-8&RRfg=xe^f2(&w^iW7$^{}Un-Vng!b31ne;c}nI0YQ-R%!Pc@zJb zo^nA9L;`9E{^Ph2Zj(n;415*fccg*+H}3{+nVn72KNI=!aq^}9w78OW&^hyktV4h) zZ<1A|z-)5KG^vBOcK+duOpkqhb|>l>^v9FYKYIAo{CY?rz`Rc}a*h%&x_$W`B_Eii z81nZm4tm7O7Xy6p9wA|acp@EWnl~Q&V$Ij@8(=N^KrYl2iHNFSSr4iFX{ztnWdGS^ zz%!6PSN*Fp@}2QKBT@fQGHr%B_U|`SZv_@OKZguhd5Dh|CXWjle3VEPoR637)M+%3b0mc{==bdJukQY!_PG zS}WLHFp#=4P%9Du_dnI{?o(O zF|JW+{gy9zq9t4C_>$yS@fEM(rW%C;M66b%vY#Yl|0GOz;@u5X;4xt=FyySkKobRx zeVE2!E$dVKghK#0rTcMGx^EYY_Lf_;C-jP1P-m&Yy!HpK?J8@5TUuOp?F2^JG}O>~ z!hsoTC+w^~%<^mDhPhZ|t0}sGBY)GgS@puvUzx2p>Lb}2u+|hNm8AxG@qRmgpd+Be z9#K8bJGP}f3kzdIO_@~<3QrqK7zQ8*lnqxEFf4u1^No|G$>IAwK3;#h_ zXr(?$-bctSyrQi8#}QUR`LA3rW;CIWdM|AyB$;3wpViayLZD~`GB!q#59=z-o`dcj z1?*)3e5Sq8e;}dW^#Vdrcs-l6c#s2rB4p81B5A5kpi+}r=2)G${T4ngZH#BYNBTDVH3>^bOWbDQFVAVmmMZR*WGV=Fm-eR*i#nLz1`39iG zdGbV{=ki^_{Omx8)kAooU(*qspwdHin4Y*&nI8JQ4T^02-A3=XBZp5va^JXd&4#>f z^GxgdSmD}I8}mwkbRyQ2I+Q`6XATgS++4yh=fKu;R7%p=in-Eo?_A$HWIAZNh^hYX z3WW2vXlw#0y6DjG(t(YOGGFeiOPO4=$pMY+X$8siK{IMi0nYak8Xp>T5_HOrd`x1* z1P1ES7hV97Wy6@Nvr4w<*2&|y1-AZ%Q$ye896EjI>x{;%q>Aj33759D%CZ}JjK<|3 z%at1irUiRMiwf08ae9X}vRMrO+vXXt&8t%hH_rgLRQMTfPGi3a)qzOtIDIIA)t#`8 zkl4J0)XKz*RkhK|4t!ICj=PqB#X**3zHi%-Lpe_!xx)}T8N6c2HaFjyigo_K9sN%- zm*CBTEKO=KLEH%)EW?VMxiYb@HQ^d_@5_(2GKK3{N^08nx6|N>HYkU;ZHH3tI*c>v z4%bnq@}%#_HgdG#>Q?Y)IX1a@WHDR*=B`|N=siDpi0FHMb8(Rw7|f4sxx2dV`gb9G z`{f<4g@NXqj=irY?%NV}zO$a|>Yh`kzQ5kH);pIH%xvP;ZwYFRWy^m0Nb(AsF^0Yl zRyfmnQDv^eEo>PQH5FthGc&$OUUq5|%8dGK?b#sQ0N5z=mYs>=VDaE6T#6{b+ zB&4U+_&%?s8h!e;>w?|Ds3y}dfUeL6t1y#xq~_g>z~rxR zoV%lZl!iWZ*4nlE%3{6v>8qUZUn@noh6$ILn6EM_W-10`!4Ke(Vw3P#H*O=ivVi9s z_*9gTX8-6&<05Kjr({pU+G#25MlDG*J#os%{u|dSn+J0>i++M=ak@oDdLjnrU&jmf zu?g7;yZG(5qXkTS7koD@x`rfOm%psJ_E-()7J4*hCPs}Ou|j#?yX0%kNSZwA*4NiX zlZICPiFvrTtN1MF&woIgqRw~i`Il~%n?DN(q2=X^5{BXt0Q)te%y# zB<8kq7~HBH^Hue`7%buh?pf3Ath|CuU&_Z(I194{(qJ(Z^<4Z^o;CaOI%-5A7Jm;p zgvt>P>Aw7=-A)X`0UFE}>Z4-LQzdoX<{60|32hQu{7C)GmFPEpDw90He=IUrUuO34$LT%0N3Z-cCR53E=^O&>(D(sCOgthb6$Rzz@L_bZ%p6#I8f27B~ zQA~kVI>R>P8ehdxF#aej5d6L~%>&%Ku0rE>h>yW7+22r;ZD zkJN*rM?}yOx>Mc&NJAbne;?@QC79%XumM0blqaCektAfaNPU zC*t9oxuoD=b~o6;MpInu%#k|DMf<%uBuj#L*|tsbs-TYy1zP2J7`xEh6@FgNZdq#2 z@bMiGv~WiZcbK4T4Ob~5$!Cqa6`L9uMSn_ZApjme{ClcaR*W)dOsRI zVT0378`;>c(>F-TkeuI43(0kRNbLzF4sy?Eb03(Q4 z0ZPW}8mzvYDZBLy_pagN8MeCP`E_Z>TxV#;?+(=931DjgiL(0LqQe{yG%-?553>8f zS4oAjL|*xX^F+H#PYbQ!@OLG|<%vsFr9&0D!5_>o@qopN`PJ$tmV)-Zt`@s7XVGsefp|J*IV-+8X4s_guGp@|dpYC{eYD!lPFE-nn$P zo2x80_XD>rmj{$}bizJp0C*zCg9nz(5gKJ`T1Tyzj(qw%1(`f~W`X%$s@d2Gf2|{` zpGOMB^L^X(JDLSn(vXoKZS(Dl*d=Kr`D>%^AD2MQsdf=0a#7y#=Czj1(T>ygTMqo$ zW+u2(3=Ib()vm3WLg^UtF~d+a3m$+C`1XqIhnknf*MFOvE%pgQzJ7T!3`NE?lq+6=_L1TGo}Z2vCH83E!9zD527Rw=;N-oGSOH&ZMLQ4Jw}82lx?;ix{fYHz5$zi~fVI@qDl2f4 zB&&$MFJSPPa|Gi@Oa6af&J>e5e(MPfMBH7{e%HT?JpX)PMk1J5wd{6=Co8G_D@e)OqrTmV~o!gYtj(39~lT=p#ut`k%bqI!Lfy=J#H;?+uiEj8qWg%J=28w zBq!^r2E?I%1Ey#{6Jp4CT(v6o#Wt-EyLsaE1*znp88z==O^^TL9187Qd7i*Jj^}|b z{|bl*@1SM)pCB}EBlY*`e+nDytBkwCXYX9xuziLs zs_|t)$RaS!0x?JBN3AYJ#^t}bHVWf`?qmTR!C)!2jj?ILDR`};iY!Yn*VFUs;;dyq zo@B+G-P$H}8Vm37Cam`EeOha(jeIBXJcegDl!#wc3?y;Es`L8<%?Z=<~tC+tTqZP?< z42?Ji{d~Z<0@5XSt6=o_k3%Fu^1`(^{#zACn(#5W3@3u@e`kG6WNOl3p1OJLI?<7B z)^^NOqeiZ_-?qoTi37dG#)L9SQbN+A|!i>apFvE-d!J-{+A*!;L`Jv~+z*`%eL!)5) z*-Vrh75FaO>6WM5=ZF8I{#Xe<3Ow&e!2o#q|E$WclBzo!dLKQJJj5dz{L?il8tp(8 z=4^xd{LJUgbyjZ?byf1Nn-&#lA0P<6^_+g?!HFvAA`-iqTu=+2t7tr2$X<-Ie@Aof z9pi{Nsb4+rIp{F9+G!X4H>{zC`q=!I$=T1vsK(H{yR9qu6_+%3AMe^Y2zU>~6aME< zRMB?SUH;jU&IOM1H@BTxJ9ssIkP>G17n;W7w1vZwKz1A5FA#1dbHD^pub}WB9bKER z!d-*VNa5;*9ydj1l%Q+Hi_WN})6d?%%ga5g?DVYeG-y&_TO&@Fr3(#hL@gt3~nPvd-U;!zBTj<^a1r!)+ zM=NvUYrtiGXT-ib^7@Ih7zA?+HeT+%cDr0W+P#gMh zBlW}+2i(=)m;U3C3U+e*jxMH=F1r3g9Mk-b#_f_9V?HJUX`=H{G=|l=({yn_Si4w8 zInAWys!e+w-brQg57eNOV*@5{mtP`!FGQMmGd3YFUl}5s#$Yv#>uo5!!luEUk~6UY zH<6Bs=FR=^!u9jyqrQto5>0EdOg*SA%iuU;$2-)iyi0BF@B6Gw!d!$W3Qr|lw#id0^3YB%t zQrj~O)Po(B8eorA>2b`@*xHd+M5(%!Lwe#|i;bXWx+LEjz=5&_vy(A?5Rh6W9M&p84Lqxw?0 zQ>)rYY#&LMbd4ZZE%`fAksHGi&D~Q=4&AoR6Ay&Mnw2ti-#(#p(_BHWyESj zREK8q@-ex3jt6;ddQYp)?->L8y~f{Ad)?K3@w2jDCKMEvXT}!0!PUH-31OW^H$C$< z;}Cqjo&Nha4Rq8wy=B( z+7fZzD=4$R+6?fLsgrR#$ke1`W|U=6>1#Sh2G+o+JhL2stp&r26?&OIygz5UCfu_~tmMCHnz9c^8R1<91U(l^{>YJe>NvwT*kOcn+9K>jQ` zI-o)ZXPb9$K&w!Y50Etax zXN5xdk%G;H+7YWW9h=9W+=4RNfAQ%6gL?zDo;+9rJuQz2yy+`L9xg*L!@-}WFJzj` zyiLuY_44@(#sK(ocs>ojUbacvn&Jmi`zRG8A(({9#c zgRRwS*@5BB5r$frKYEBJCN2wLGfj2qdb*{Tk zf76=sHqw;$g;7+|XO;MA<;lTr>x`PoE%xfwQ@RjO4%vA5?ZFKE-z}?~6M9Egd7md* zh3Yiul(~(9{eekX#A*N5)>%MTx+crC{U6}rf^Tb3`c+5X88+}%4G^#{EseXVmz8jg zOpRaC)rEQeDu6eaJF=vD`$HV|WOEXM)r2S2JcOu~z9xd#E`04X+VttLGQLhXcjHw+ ztqIrIOgGh@Z_J2$j;6vgs*wfF{}3JOFs|~^`oF+lYejP@`N|GM#?)o?c*{3 zu|iXfUL>$8;F~so7>mtI74>u3F%?*w*}@Z0lTgo_)VeoQD}3;nw`goIAuSMEpW8 zqRKUAeraWRqI2UH&@DCLEwFTC7B)}Ab*mI~qzSXMPQBVKPuqM2_^$l|oDVb+**Tv@ znwPaHB71s_+`VOhJ7oNwt@UnNa+Z6=e7M4COgpZwrG!+_jHbvgJ^If&XKYo6Y}R;@ zRg6fx?$ue~X+#ou7H{l#L$m6co2z5X2^s0I!2-1c=qYbehplGW>0A z<1_y~!_~dilsh__`8}uU=J~mo%c2X|zvc*pA^zNH=dPP8@AqBNdnSPQT0ByO5)5r6 z<^ur}VE-TfPEkVn%7mq~13MOThbl_^*z=Bt&&ocr&MRxlyV;eMIvcyl(v*`}A3bUA z;Py`RpJ{v5c6I+z;vBOTp0J%#+)_L#n*VyYGu?U9J0xvntIqrL<7xQ~+^VoT~#Fnfh_5fCXz5TvoTt) zoh&YP$`qb?XPE>+U<<~_l>_q+8jnSW@d9yPIiKh8sF_IEQry1l$l3R{^E_IkT7Sw9 zU!4&Dr@ZfcYpQz|4Wig6B3%Roks`fA5KyZ07nBydbm@d5p}vCjBE5#*qyz;*Z;BL= z5PGiyN$4$r5X#;B&U5em2hMZ8oDU@X3A1O-o;_>UteIIel&hMu2?`7UwBR)qv7a*T z6It+H(|8}C@n{_N{W5UhKPyB3k5rqfAe3H-nv(JqIIgwUGIu=PV0I|@1e3~3Z16Es zRV2a7z=y)8IJN7n8+Ka_@d(vrP`JW5(f(k2hJRpxj{{Yx*n*G>&qxSobts+wGyEH`jLk(&69#arDf4=3;N-clqr!>1Z<6#z5 z-=kqeDPn3d1jg@?4O<(B@5D{2U@e%fhj7_(>lqO)i0Ucuq;5qC0gmo>M) zzY=rzQT?3w+eXrJ$VkN6GoXs35mdpi(n zt2D}MkM1f1-1hb%{38%^)5SZi$0bzIIQ$E3HFT0L@Fz3L?dMR9kie69ng&5a`Du%w zNIPaxat>`JG}?_;gtCbYWf_u@q7MOzTZ}+>Y`lLJaIqD1g@lyQh*rE0cF{l=|F#|# zp)@r!dzcRxM$y2c?nIZRaCfssGKLi<yR%6M z3?9h@ixQ3#i)sNs`S}e={|6R?>)YWyY@XwWkEO&Fpi_Mqj4s5n%FD1V80)JVFmqT` zsX7I5;J$VDTjUQBN+Gf=cniYV`uOz-eGYBb7v3ya3kuu6aky1qRa>6bGiDnOt@7WP zj?||P`1B%1EBErLjszY#a}yKF@e6rSFiL>}2!=Rph1ReCc|9HU8OSBYh^_l#YA}-I z65W5s4OxgwY7ILyZz#DRGxi5pEU3{E*nU(Ub>Ur;o7W`IudN7id1F$~K($m9Zj=Gj0=)c_*kF=ZA zaSGO=>!U5N7~GoQ^Rici6hv|)Egc;6kEOSej&J>Je*)K>y(qzEordl3f+Sx7vc@Pz zerH~iRwWJ<-uy-4BqygzFykdhvA@u{rz5#u|MrLq$KoqsO$xj?S1)KR?>OZ|Qx`eE z9}#!0t8}^oPT5iocPn*KQi25UT}zqHN`9!8>0@P-<)@WU%uOJXqTn^R7@h9l9D|hn zTM03d<9AtC>enHaMt3axYcAe~SKwQB>ON-^GI*r0Q9%CntLUL*@wIgMI+T6C#sI_- z1+E9{9JSh4rRWM&v!M^esy;{vF*KJy+0M-Qvi|(=6J%-cRd7`(5cMp6MDfgXi#or z4BKf2M&&t=Je?!D;H%h$4cb~lx%#3z)7KT;*4!?xWP@0s^q^7NA7$cxCVH2(ezn-o z{nUdYOk%ROuu#0vyXbR8|Okv2jOo^ zhllFiYsRsoKzbEU^=6K`L9ABZ$@%O0l7DkxUB)_RJJ$& zBGk~=vnT61BcQsyS#o4fL)GTs?2JQ`Rk!~a``$%r2N}f2-QLzz$V|YIwk9|Er(%Nc zvML$r%XWbn6ml3W9q$nGtz;| zj?<>hgq{JRlO8- zyZV(fz4C8Cjh43RYW-e94rd!dT(wzN=8gZ~CyxN}d;2?H12h#JXZm=0E?by|U`ef{ zQ+E&5>YO6OQPN)D%RW}LUC%O*Y;Ae=Cz?*t)9tIdF8?5WygtvwsjtucxuVDJMq05z z2iOh+*gY5T`0%aGRnHQwbSkFV*Z-Vz8(lN!_ce2nXKbDhbSe%m{F&%96%|=Zhbmw} z_z#WSH)$ZM(#+SA%9214yb2@E$ ztC^UCrbDjTJ(j)mb$rJE^cIM5^aal{>bZisiJ4J}9a~gE89sW8)*L6TscKIJ6?Jrx zW<1PXzU}YAr6ez5GV)!gUZac6aR%?|xj-W+*YN^PI`|;~T5HJfnA&?S>MosNDv(oW znt9uurxmQ+9_jY&hRjtBvLc5bO(q;H+eyEyAKnc3^pA*S2x`U6v`*5TgX#cx-b?};cSJOs*?jBJ)Z|EU24IvFOE zwt-Y(oy>gm?A%FSZxNrFr=Va;{9y|TC=6Qrf=_1gBw+ERIC*jsMOBn?&+(85S*|Sc zHCmUc6l^*FhNvZV-t=DtuX*s?zS0}ffk418X>V~iB)a2%NC z1IU6pu-&OrGfdp7PU>z~S;>kNX_w$>FvUlUhd1>?tj1!xziX&TPxgs53_MU!!RDNB zZ53HL;m!CkMXCkr@J8A!BA%kpe8*BwCE^;zh0wo&YoVXSE*K$)zzpSk^r8|Nnymnv z8)W&Y1qkiF`<8w4J8*Q}6D;(5fDYE~);I~MO2EcuvLJs}$>0!Ybf2}l+SNn^@Bgjt zg|(opaHZ0nnY{c_D7UiMOpn$Jdi9w${c^=`hz*KPUx4(hj2xfcRBaNsw50yBCj54yI%8r4 z=ag-3G7;l|(x{y#J6%&DCGM=hUKZ^X@xcdTNRrfI9IXhY3qnq$1i>;KdMOVmj;E84>$Fe~do&t8^y@W}z zXJby3Ghext8o1<3>n09QKia=i1Ty^&X`_`M{GHA;m(E1S)tURn6dJ=gfHVp(E62)V z^atn&(9*aWI*mlr;NEGsKF!*{{Aa110Bo*==ABr$Uyg)-Pw~MA|DD*VBmm zYa7p*pf5O!2P~uM=nR1RUBiF*?Z*rQ`8CmEYj#C;!A#>NOE*C)!l)rVFWfuInKA(- zA8z@@0SqRTd)myr+LlhDDEZaWX*aA%glusU8041wBdIzLjoL6>>r>t{D`J>f$SBKV{oQ1rsyN(n% zy>LXgo3+9sQrG#?|IKD>``|uY_qw=UZCGqLqdZGu#DF1F0_2^%&G`)ElyydGI^LJ? z9A57xZqCdib_FSmAFMs~4|1)x{Z}mevaHcWL|uH{8He}haoc5pji$dhqvVr^BF?jI zSPFrXPeouLX1Y9R#-m~bV0Fc-i3vt($YDuz2M{5IcN*xID2=4`O9LN9# z>ClJ%Xeq-iqn#1Q`HwTu2`(>t&H-xfVm6z=J3#vX$QPn6LG}XG2V)4U2eC{{;=|K@ zOw#Fok@>#F7L#H@H8(m_MV=~@LQ21T+J7_|Mwr5O+$G~gmxOC|pU1G-P7G8^kpeqk z;V8CZWB@q^^Q#F?_O*Y&1lSGnC08jJ)8@yDf^U`ri?yle&MhjH49H)=o3|M6C!efayz()D zTjxG&5&i}GkX@wL>;2-Pa0nUn;WII4sqTCIt6Pg&e}*_}s-FMg+tZKO98#2z`r{I! z3+CrnQR9AA^j58QBD3mG%P;sqt08}+z3^RS2PAMhq`DfhT_{46DZ#q~lo(?MR*%^y zlYOt4gheUm=LSkfBPW_yNa^OK^G$_n0nL#z(#~RMZ)?V%+P-_&@~muSWvsCm--=AP z4>lt&*m|Ckq4{#6Qm?4ZT2Y=O>c>B6nvOL@+x=)G0dq4|Zup2um4qwxs7OB=)Ibcl zPWW_k^G$)7@O?nH|GIF2nc%dELb+|#y)MvwR@&$_PcV0z8f7iHf@w zAN(D$q4-+3CLEIt;iawgZ8wa#>S$|;PFMw_r7v^JH{z#_5_z1t7(cTS)x6Hi;~IL% zt|Gj`_3>8!K`d*R)<=_>b`jFQ;P$;w#H(y+VYj*{KmXj14H1kB;fi<~9QEgQJyFI% zxP9bX_U8qBkY9=FI|EuixeH1L_D;-C0s=8Dz~i*zEHliku$tCif;E(vlh=H zLByLeEj*DSeoBj{BI-=}2l3wExrnF5T~jgzHEzc$amdeS4v8fWy>ZURdh#v(QbB(C zfRFZ}V=Qf2BfIKDtR2>Za$Uhxad@K`HO%lz};uu|DjrHSf4>P+uJ$p2|79<|{<}%SscGuT?@Z^`-Y0J2) zKFh?@g+VE9SC2ud%{bSEMxR}4;fzYinnyKqN2Pjymf-$r-l`6Y*sj>kTIW?CwVqUQ zD2g5aWQOrQ%9ZWMx6UYUQdQORwz!MBGCDus#A4_Yy*B^jcrL7w>0{j_o!61SU7~9> zu<_{kb2h>4ics2I^#WKQpDakhX8wVyvsVLjpb@n%^K_%-Y zNb+A$B^?cpj7PBdR?{+Rxg`!WCpYkJS~1_0@>6stW(X(fv*SB!+3b<`{ytQ48{|tn zjl+hemVeqGl&PC#4o2)J(KsaH2X*isgKNIu3^65ZuN;q{Dahkh{N=G?xSY>tJgEs4 z;Z*3}(!n8B`!gi-Biwj>h7bq(uz(AU;cpG7y6X&QY%S_KK4cQC8+y$x$6lKzElqda z-K6L(wQ%%PwPT?g;(Ae;OmB71)Ns~B%BKjMSY4p4wM%flw8;@j^PLg_5^`JG<8Qm& z6-yn;k>leP1p7b-X_S_$8A}t{eu6#Nvb$}Go*zuS~>0Y`3Y1VwIz)fo}TZ_$5wb7=*STl){Rp914`51J_s$Id99qc z@E22w?7{dBi@$EJeh3TRL&!`t`z_Qe%wcaeM-;kl=Fe`}ZEF96te21pA4BU6HW`?= z%pP}ekBA8Biw2DJd28A1&R5$gGw2#LW(vC+IT%m&uR>``{Cy&kK41hy~yy~;5 z5rc>FjHav2+s!Gn??$0HxoCk?tTB@v+qI53BPQ=msfV}1Zl^VT>XqoI`FDbGa&E!k zZX5EUvUD-|+lcbA^?xlcZyle^d0r97x6*#>aq4a6R~*$EeY>rO_BDG8nE&U_#KAnV zB#-E8{p@-v78)0iONn7PH^7H=^zPf|qZ=a*iH}j(iOfB(G)5!GG+%eo+#_d9HeT+t z5Lk1xEPFdUHuvMY&M|5}dPWNSZlMofJ8`DojdyO$mq-aa8B#LaI)J=)j;T>iw079Y zHs(L<_L!C~{dPadUC|-v{zmS>`oX_Fa<>G@!_Qi%I-l+LTre`dA^j$U|GI^t`z$^| z4N*}V_ioGQ!R!2(3^LSt7>HedwK3^SRiU?6AoO4i{hdyS3Ls5I$(pTLs<7_8*2Fx<8l&|vVQfFo-jtbx1Bg3MRH z<80_ebLUYsymmek4WT1|C!b@Nn87{@N(W`&(PvLJav-Y z%;=rMM6Zqzw2v_82!=H;p@cn8EzXn%X$l+x)pkH7*G7PZ0{;5T_x zg5Ac=>B}YM{JRMR%uVPxqpc_%Vup!@Ju8LXc%HFUoI6*_7>BO|(<)bGYy`rX@&$62 zX|cYrnv*M7OK1gqjT2M+Kql~5#v&kJ4+(|8V!1JL)2~iFDV}z zl0G1UbiV%I_Z@$|kf}R4`$G_IjtZ7{6HPT4z%I2!hL7|6A^n(!rjIOi*}TY3a!p-d*A zhi2_syKQkUO#96*^bp2-G2yzF#I#k}iX@Y8lc6pXWt4WCm3*PXiLfs#;PoSjz8Vn; zeh!eSnZf`OufUaEFB>PRqipU8Nb!|r$MqKiahOAs+f|>3zDsRJPofx~Henyq zd(Da<7R=gP?gTo+V7R<$B_&r1nRHY0%OUds!B(dQZ}0lPnw*XwU}4Xyz&JJZS}8#0 z%r%(?QonU73VhH*|3;$Z?6>q(%aQnDe{X+(E8$-?y!YiW&V!Lq1TCEp0{L`Hs?1D( zVn%+GO<<5Vy7SZWqhg1deg5KdAq9g`=&mIYq;vv8UXu_p1@+>ng=Y%4OR!KK)6J6F z+V1J;A3nvvY%~(ju&$l<1jAEThFP;Eod@v#q|Mm#dZP1M(x8H6<5Y4P3K4`Ivxbh*^5JqQNErVp)YUT;g*Um7S!Za(uw8y65>&GZOGP~g zKUYX2S=C|*E%2$>n;wH?T^C0yEv=|X)8@Dy7FE(>Fm}hXY8*oM$&CN7wQp97YVMP+ zT6ag2q*Pmw|H^A_n;7@OTg>$93@;gPhz7&LQau{kBozn`VJ~5+gEUgGcWk`F6zMye%Rw*)Wnr)TY~m>}_{ys5-$xB{+h z#)Ibz(o%4=?Fo7iMlr%8QQ@o&BLPD0AT!?#nkxq*rEoj@26bxYCzm=i&o|aR6K#w# zOVq{|Mr$L=)H=*C?W_!Ynyc1LyfH#O^v<5`yvMqN6#YXAX7%hkkXb*@(tllf9*v3& zGrWuhku90Pz(*k<(8H?h9sw(oDz0Jt=o_9D<`wiz^!lwed1NEWISmHX>qtZy+7);^ z!Wp(knKtX5!m72R>Z78XH?)lov!!#@_f={GG$>W1`j(hy=&fSJdhD&DT8CzU&w_}K zQnE|g2G{uhq2D&lf4#L74_dztPGDrtCA3bUtTMznrC^e;`R>_;PZcI7hYTmHR1+f} z(jBAMYI2X8!8}5rk@Z~nI{F}*nI#UH)()9xPH>LPRCdjzR7LT>{~S+gOCSTaTpq0as(CDWLlZPCRp}^R5cFHIxFya_}eQ1$5`|6LhlxIdXuR2rs5ZC)3eFtew#3RpY zA4*;Ma8I<74!vV{({}ZIeGjQ7jO0NCK*o3Pu;Z($uyn*x)}%~bTAAXPan1R4=Wzv} z7!Uj>IczqSLq#aDmuPZqB~+i#J+1ai2w^-XsrHl;vHfsONIzVM8uRj*r%#v`U|=Wh zRJR6YC33BOtPR|*aH^V@?Es9bJW8TRFKZ!Tsd_PHHn_`*Tqj$cnv0pzYEOFYBPN%Y z-G$pn=fY$6jQ`O#_Us zCn<;g)ovO2s%TKb7UVOCUZ_J^>*^v10<(yJK{woZH}>HWX;z;(t_7lC5B0cu}Eu z|4)$}X#V(6do))1R|ra#II`p0J~7X#{94ZQ3;j~d~ruWmpd$aAa*Biav`HoGwMJ3 zSC^l+ERWitvJ!r?>lrxGClgvh44}rG9W2Gzj6HU+_jYqRu5Zr1Qcv{{T?pfXvb4m9 zJJzFpez>y9hN@8J>>MRU)l)OoTl|4^1sdq_U0v5Nt_e9rq%VodEb_L+!DUh8JNcT= zs=3iP|E84iLzi!V>*l5q4-dJFw z`%E#W^CBsWXzN{Rt|sWKsZv^pD^m>fXU3FrQ^tR)^zH9Bm%tr!{F#bY0_9nLq_FUZ zir+Q?7g8I_l{#!J%8s>rtL_=p8c6MI0TgY~0g`7rUXFuI&dw>H91WH1oA0HRlUp^G zEH570!e|P-W9$P-EyAOz5mh4dUst9LPPVc|Y?r<&Zuc+Du1`ZMkd${Bhh1juU*0uE zq0Eko*}%k8K+a$AMOlx10|Z$mqTFDI!{E1^ZE+=X1~$P9sfCCU4Z=}+z&&%OR7gP0J*Z{IAmk}wJ)e+366!g_3+F-^R#lIEf5L5|0~+-`UzLYWZzAn1Ge8(UmGj(79vGcd8FD8QePmRz}^ zgnXr+_Jy&(YF^1DuvOp=Z+Ib}hgVLpA%6bsal=sr=;`t3E!3Sb!@Q&t`eP}ztK*Rq z=6X7c_e?7vev9U5yx?cd4ewYitHeKc%)mO#0H?NKByN@@7hbnO3FVx0^=q6}2B>$Q ziE#r%jM)+Q?ghJy-;jRRFNM_(iq2MSWFPZrqNYqK+i+6OD~ojui&b^3YGL_|BVGEa zbjf!MB+X^z$)_+m=*p8+7aG3)m2}<&lZt|xi)={na(*m6V4-QutSLV)pR3vLR2vxw zkE(I&nXmDDQ5-Xc-&I z&Wo|SyP(JVi{`IEZuF(UvjElhL;EqQ+6bJXIVg4N*3po(dy09AF*t63q(kxch)=L1 z(=Dj^iOFx&NZNSpH`M~J9HLeuAmwL4bj@S9cI#+|uL72cXNKY08-3uSsS7$_Erz*4 z)WbcCPXW;peOc%AH#(8Z!6bVaI^cq9c$$<(9h}4O8>{v%KVcLEHss zy1Bs3;W|Gnm*Zv@XBs%s5(>D@&FC*ZA3<%T#b9{wlO1mVE_&K|qco}>Yr7sZxM~YZ zBO`j$SzKGq-WIMCb3Z+*-QPQYkPW+Q)(ow!Q?c_93098=44#8W;xnu%L*l=LvB!Sg z4?R8D)RG@owa^-Hiv?OxT6y`i#$jzQ3wI~8!yz=j| z*>UYRbcv-a@9+>WLaaT^yZ-a**^HK_WVfRNWV@zTm)kW#7Nh_yB0Bwr46m}^ni;YB zf*OX|8-(?q@1Tfe=sZ#c-mRwIp{IH}Ia9pLH^)R~r>SKUHc{g1t_`r?>lk_P-+Rk+ z$wkERJ<#te3hzjQuX|vOAy?N;G2qq^_H|S8GRjB>P%|Y^oLOb`Yj4~a&_0)GWv&I2u^)I~Pn2C3X1gH@KkouLX z6f^C#qnHiL^HX0NOpT_Zl+I$|ZV0%QG{HWf;yLd0O_EC0_Asj;4lIu6;7jAY{hu*9 zZ@F*S!#vs9w0<5hOt1~T-(!Y`dnTRo&Dlm`^DK!e_Dk;CnP0}axw?4M)tB$CeWGre{nScpq{*) zX!t(<+2-sE%LOB)FXrS3Zm=78s-1iFOFIz1x{6;sKMvZMr2#-A1QSWl*5faFtlx1` z5%9f%3W{czn+A%jJVD1uU{ey%a@`e-vgbiwZRLl*%d@4W1;`iYd)vG`?S}_E6mzZn z;~i&<{{`g^KrNn3mAWpr7bbH(Yj9okU9`&8dgFy1Z>erOhAkohR4=oIZsHaR7d{s= za&i!i%GH)ZoZRWJb-Sz0mKb;Qz@5|f3ji$Ge%)&KBB9-xza@O(Ktb+uf5oSL#g(o* zJn0vwi|k(owu5cgE6v5gYm{|DqC;#R-a+6j6naOSAb#D^%v(4S)MlvF9}ir2F2}F~ zImJl_*DQ3Cw7NUws~>m`L~h4U1V-2X|KF7$|L=DQox>?503(_G?WR3El?CW^7Tm?4 z8qDvh`+s2~lSr;xq|sTi6V0R-o!$M)rAvrDS9Xm67bWKLW$jbw7U@ba_AM z;8fr|GU9yR$N!3=l>lcs_0zt@qGe~B9Lq}OF30FP@N;cRZzz42F#9U9=UrFZ9aol% zS^yII6iO1@$VF{8)o#ElP%n~Yt_j-Uo@Qu%J_427$1h&>=LR{8!_Pxl2um#fJJA7t zz)coTJydyVr#;Z70K!D|Wxqh+Ny{SP?7jeUi*kG6*ZQQn|2oi_hBNoWd4ho4$@zVO z_OXWBz>Ph&FE}X!w`ty7E|l6)o}$p_E(B(U^Xc56?f=O3)xk9S#2MgTFy$i!;(!t@ zGs&}ak)Xe*)79?q`B}L#x&1@A6K=}WadW~^0GVs@!pUF1%RdzYQGLP!XZ5!LPTz@T zeHkQnF>*D$NS*Krd=Up*ZaH+l7~_#UtCl?+U>-e0%Mu18+D{JX+IN|>+fV4O<~sr~ znEl~5EWTH8X`kKJ=7w;9*3OjAvd0T7>$8{|KiP}IVlaWk{t@41&# z;j(+HR#z+W?IS+D|KUF%)vjgINNu^jv8ODs)sO}k87?-T-S+(vXpk>qw8M-`Y!vnm z^)r3o?5g2QEtOS?a>HChL|>tg*=bIn-$bq~{?8!`Y|ZBI{Hpat;L@ho>>BU0qc1^t z^?+FuZ(w_*C8i*UbGiDkuPt5gDmvM1LYE=-20(V5oI`~-py;qkU| ze=o08JOQz6>;d!xiv)}JyUXe`M@rwRwhQ;e<#w5ZiygW1|C$Yrv;au_9ExH?8}AIf xGtk!e5Bx>f8ps6nKcb)i+ZF!*CWL>l9K{p3t~$T`1H3Shs*;u>Lf+!z{{cOS8~XqN literal 0 HcmV?d00001 diff --git a/mkPackage.bash b/mkPackage.bash index f61581a..f1655c8 100755 --- a/mkPackage.bash +++ b/mkPackage.bash @@ -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") diff --git a/pom.xml b/pom.xml index 0da05f2..a2caa9e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ + 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"> 4.0.0 edu.jhuapl.ses.srn Terrasaur @@ -55,25 +56,29 @@ com.mycila license-maven-plugin 5.0.0 - - - -

com/mycila/maven/plugin/license/templates/MIT.txt
- - **/*.java - - - - - - - check-license - verify - - check - - - + + + +
com/mycila/maven/plugin/license/templates/MIT.txt
+ + **/*.java + + + 3rd-party/**/*.java + support-libraries/3rd-party/**/*.java + +
+
+
+ + + check-license + verify + + check + + + @@ -126,7 +131,8 @@ maven-surefire-plugin 3.5.3 - -Djava.library.path=${project.basedir}/3rd-party/${env.ARCH}/spice/JNISpice/lib + + -Djava.library.path=${project.basedir}/3rd-party/${env.ARCH}/spice/JNISpice/lib diff --git a/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java b/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java index 1312ff7..502a689 100644 --- a/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java +++ b/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java @@ -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 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 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 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"); diff --git a/src/main/java/terrasaur/apps/CreateSBMTStructure.java b/src/main/java/terrasaur/apps/CreateSBMTStructure.java index be9ea69..f0f44e7 100644 --- a/src/main/java/terrasaur/apps/CreateSBMTStructure.java +++ b/src/main/java/terrasaur/apps/CreateSBMTStructure.java @@ -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 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 intercepts = new ArrayList<>(); List 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 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); } diff --git a/src/main/java/terrasaur/apps/FacetInfo.java b/src/main/java/terrasaur/apps/FacetInfo.java new file mode 100644 index 0000000..b4d141f --- /dev/null +++ b/src/main/java/terrasaur/apps/FacetInfo.java @@ -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 neighbors(long cellId) { + NavigableSet 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 interiorIntersections(); + + /** + * @return facets between this and infinity + */ + public abstract NavigableSet 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 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 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 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 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."); + } +} diff --git a/src/main/java/terrasaur/apps/PointCloudFormatConverter.java b/src/main/java/terrasaur/apps/PointCloudFormatConverter.java index cfb7954..b53246b 100644 --- a/src/main/java/terrasaur/apps/PointCloudFormatConverter.java +++ b/src/main/java/terrasaur/apps/PointCloudFormatConverter.java @@ -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; } diff --git a/src/main/java/terrasaur/apps/PointCloudOverlap.java b/src/main/java/terrasaur/apps/PointCloudOverlap.java new file mode 100644 index 0000000..911885c --- /dev/null +++ b/src/main/java/terrasaur/apps/PointCloudOverlap.java @@ -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 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 refPoints) { + List 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 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 scalePoints(List inputPoints, double scale) { + + List 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 translatedPoints = new ArrayList<>(); + for (Vector2D inputPoint : projected) translatedPoints.add(inputPoint.subtract(center)); + + Path2D.Double thisPolygon = null; + MonotoneChain mc = new MonotoneChain(); + Collection 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 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 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 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 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 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 theseIndices = pco.scalePoints(pts, Double.parseDouble(cl.getOptionValue("scale"))); + + // now relate this list back to the original list of points + List 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 diff --git a/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java b/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java index dc0e03b..529cb3e 100644 --- a/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java +++ b/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java @@ -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 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; } diff --git a/src/main/java/terrasaur/apps/TriAx.java b/src/main/java/terrasaur/apps/TriAx.java new file mode 100644 index 0000000..7523347 --- /dev/null +++ b/src/main/java/terrasaur/apps/TriAx.java @@ -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 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); + } + } + } +} diff --git a/src/main/java/terrasaur/apps/ValidateNormals.java b/src/main/java/terrasaur/apps/ValidateNormals.java index 8fc85fb..6397eee 100644 --- a/src/main/java/terrasaur/apps/ValidateNormals.java +++ b/src/main/java/terrasaur/apps/ValidateNormals.java @@ -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 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 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)); } diff --git a/src/main/java/terrasaur/apps/ValidateOBJ.java b/src/main/java/terrasaur/apps/ValidateOBJ.java index 8e4c8c1..d720480 100644 --- a/src/main/java/terrasaur/apps/ValidateOBJ.java +++ b/src/main/java/terrasaur/apps/ValidateOBJ.java @@ -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 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")); } } } diff --git a/src/main/java/terrasaur/utils/CellInfo.java b/src/main/java/terrasaur/utils/CellInfo.java index be5ea78..19818be 100644 --- a/src/main/java/terrasaur/utils/CellInfo.java +++ b/src/main/java/terrasaur/utils/CellInfo.java @@ -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)); diff --git a/src/main/java/terrasaur/utils/ICQUtils.java b/src/main/java/terrasaur/utils/ICQUtils.java new file mode 100644 index 0000000..3deb20a --- /dev/null +++ b/src/main/java/terrasaur/utils/ICQUtils.java @@ -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); + } + + } + + +} diff --git a/src/main/java/terrasaur/utils/PolyDataUtil.java b/src/main/java/terrasaur/utils/PolyDataUtil.java index 0c7e331..8071490 100644 --- a/src/main/java/terrasaur/utils/PolyDataUtil.java +++ b/src/main/java/terrasaur/utils/PolyDataUtil.java @@ -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; diff --git a/src/main/java/terrasaur/utils/SumFile.java b/src/main/java/terrasaur/utils/SumFile.java index f96370e..b4fda39 100644 --- a/src/main/java/terrasaur/utils/SumFile.java +++ b/src/main/java/terrasaur/utils/SumFile.java @@ -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 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(); } /** - *
-   * return new Vector3D(1. / npx(), frustum3().subtract(frustum4()))
-   * 
- * * @return + *
new Vector3D(1. / npx(), frustum3().subtract(frustum4()))
*/ public Vector3D xPerPixel() { return new Vector3D(1. / npx(), frustum3().subtract(frustum4())); } /** - *
-   * return new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
-   * 
- * * @return + *
 new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
+   *        
*/ 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 + *
Vector3D.angle(frustum3(), frustum4()) / npx()
*/ 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 + *
Vector3D.angle(frustum3(), frustum1()) / nln()
*/ 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 isInFOV(List 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); } - } diff --git a/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java b/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java index 7285c4f..11f8ef6 100644 --- a/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java +++ b/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java @@ -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 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 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(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 getDistanceMap(LatitudinalVector lv) { UnwritableVectorIJK ijk = CoordConverters.convert(lv); diff --git a/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java b/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java index fd82753..73e0ae6 100644 --- a/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java +++ b/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java @@ -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(); diff --git a/src/test/java/terrasaur/utils/SumFileTest.java b/src/test/java/terrasaur/utils/SumFileTest.java index 2bc525d..400d434 100644 --- a/src/test/java/terrasaur/utils/SumFileTest.java +++ b/src/test/java/terrasaur/utils/SumFileTest.java @@ -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 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); + } } From 62121442154e07c1a9c5a3d4abd3931f432080c1 Mon Sep 17 00:00:00 2001 From: Hari Nair Date: Wed, 30 Jul 2025 12:02:54 -0400 Subject: [PATCH 2/3] update dependencies --- pom.xml | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index a2caa9e..a916cf6 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 21 21.0.5 - 2.10.1 + 2.11.1 1.1.1 @@ -139,7 +139,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.1 enforce-maven @@ -213,7 +213,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.0 + 3.5.1 generate-sources @@ -228,6 +228,18 @@ + + + com.diffplug.spotless + spotless-maven-plugin + 2.46.1 + + + + + + + @@ -270,7 +282,7 @@ commons-beanutils commons-beanutils - 1.10.0 + 1.11.0 commons-cli @@ -280,17 +292,22 @@ org.apache.commons commons-configuration2 - 2.11.0 + 2.12.0 org.apache.commons commons-csv - 1.13.0 + 1.14.1 commons-io commons-io - 2.18.0 + 2.20.0 + + + org.apache.commons + commons-text + 1.14.0 com.beust @@ -300,7 +317,7 @@ com.google.code.gson gson - 2.12.1 + 2.13.1 @@ -321,18 +338,13 @@ gov.nasa.gsfc.heasarc nom-tam-fits - 1.20.2 + 1.21.1 gov.nasa.jpl.naif spice N0067 - - org.apache.commons - commons-text - 1.13.0 - org.apache.logging.log4j log4j-api From 72c19fb94168f193ef9773a674da3379ef4c56f8 Mon Sep 17 00:00:00 2001 From: Hari Nair Date: Wed, 30 Jul 2025 12:07:15 -0400 Subject: [PATCH 3/3] apply palantir formatting --- .../altwg/pipeline/ALTWGProductNamer.java | 603 ++- .../altwg/pipeline/AltwgMLNNamer.java | 320 +- .../terrasaur/altwg/pipeline/DartNamer.java | 528 ++- .../altwg/pipeline/NameConvention.java | 30 +- .../terrasaur/altwg/pipeline/NameFields.java | 11 +- .../altwg/pipeline/NamingFactory.java | 166 +- .../altwg/pipeline/ProductNamer.java | 14 +- .../AdjustShapeModelToOtherShapeModel.java | 364 +- src/main/java/terrasaur/apps/AppendOBJ.java | 218 +- src/main/java/terrasaur/apps/BatchSubmit.java | 115 +- .../java/terrasaur/apps/CKFromSumFile.java | 459 +-- src/main/java/terrasaur/apps/ColorSpots.java | 930 +++-- src/main/java/terrasaur/apps/CompareOBJ.java | 1892 +++++---- .../terrasaur/apps/CreateSBMTStructure.java | 393 +- src/main/java/terrasaur/apps/DSK2OBJ.java | 314 +- .../apps/DifferentialVolumeEstimator.java | 1804 ++++----- src/main/java/terrasaur/apps/DumpConfig.java | 86 +- src/main/java/terrasaur/apps/FacetInfo.java | 381 +- src/main/java/terrasaur/apps/GetSpots.java | 781 ++-- .../java/terrasaur/apps/ImpactLocator.java | 1317 +++---- src/main/java/terrasaur/apps/Maplet2FITS.java | 1447 ++++--- src/main/java/terrasaur/apps/OBJ2DSK.java | 1112 +++--- .../apps/PointCloudFormatConverter.java | 906 +++-- .../terrasaur/apps/PointCloudOverlap.java | 652 ++-- .../terrasaur/apps/PointCloudToPlane.java | 612 ++- .../apps/PrintShapeModelStatistics.java | 90 +- .../java/terrasaur/apps/RangeFromSumFile.java | 695 ++-- .../apps/RenderShapeFromSumFile.java | 1443 ++++--- .../terrasaur/apps/RotationConversion.java | 393 +- .../java/terrasaur/apps/SPKFromSumFile.java | 176 +- .../terrasaur/apps/ShapeFormatConverter.java | 862 ++--- .../terrasaur/apps/SumFilesFromFlyby.java | 730 ++-- src/main/java/terrasaur/apps/TileLookup.java | 573 +-- .../java/terrasaur/apps/TransformFrame.java | 231 +- .../java/terrasaur/apps/TranslateTime.java | 402 +- src/main/java/terrasaur/apps/TriAx.java | 219 +- .../java/terrasaur/apps/ValidateNormals.java | 434 +-- src/main/java/terrasaur/apps/ValidateOBJ.java | 645 ++-- .../terrasaur/config/CKFromSumFileConfig.java | 87 +- .../terrasaur/config/CommandLineOptions.java | 411 +- .../java/terrasaur/config/ConfigBlock.java | 38 +- .../java/terrasaur/config/MissionBlock.java | 31 +- src/main/java/terrasaur/config/SPCBlock.java | 7 +- .../terrasaur/config/TerrasaurConfig.java | 109 +- .../java/terrasaur/enums/AltwgDataType.java | 3430 +++++++++-------- src/main/java/terrasaur/enums/FORMATS.java | 104 +- .../java/terrasaur/enums/FitsHeaderType.java | 41 +- src/main/java/terrasaur/enums/PlaneInfo.java | 254 +- .../java/terrasaur/enums/SigmaFileType.java | 187 +- .../java/terrasaur/enums/SrcProductType.java | 149 +- .../java/terrasaur/fits/AltPipelnEnum.java | 506 +-- .../java/terrasaur/fits/AltwgAnciGlobal.java | 84 +- .../fits/AltwgAnciGlobalFacetRelation.java | 117 +- .../java/terrasaur/fits/AltwgAnciLocal.java | 84 +- .../java/terrasaur/fits/AltwgGlobalDTM.java | 110 +- .../java/terrasaur/fits/AltwgLocalDTM.java | 189 +- .../java/terrasaur/fits/AnciFitsHeader.java | 4 +- .../java/terrasaur/fits/AnciTableFits.java | 480 ++- src/main/java/terrasaur/fits/DTMFits.java | 596 ++- src/main/java/terrasaur/fits/FitsData.java | 328 +- src/main/java/terrasaur/fits/FitsHdr.java | 2058 +++++----- src/main/java/terrasaur/fits/FitsHeader.java | 1204 +++--- .../terrasaur/fits/FitsHeaderFactory.java | 336 +- src/main/java/terrasaur/fits/FitsUtil.java | 998 +++-- src/main/java/terrasaur/fits/FitsValCom.java | 47 +- .../terrasaur/fits/GenericAnciGlobal.java | 44 +- .../java/terrasaur/fits/GenericAnciLocal.java | 12 +- .../java/terrasaur/fits/GenericGlobalDTM.java | 9 +- .../java/terrasaur/fits/GenericLocalDTM.java | 9 +- src/main/java/terrasaur/fits/HeaderTag.java | 262 +- src/main/java/terrasaur/fits/NFTmln.java | 265 +- src/main/java/terrasaur/fits/ProductFits.java | 693 ++-- src/main/java/terrasaur/fits/UnitDir.java | 33 +- .../gui/TranslateTimeController.java | 172 +- .../java/terrasaur/gui/TranslateTimeFX.java | 49 +- .../terrasaur/smallBodyModel/BoundingBox.java | 426 +- .../smallBodyModel/LocalModelCollection.java | 285 +- .../smallBodyModel/SBMTStructure.java | 194 +- .../smallBodyModel/SmallBodyCubes.java | 273 +- .../smallBodyModel/SmallBodyModel.java | 1010 +++-- .../templates/DefaultTerrasaurTool.java | 72 +- .../terrasaur/templates/TerrasaurTool.java | 334 +- src/main/java/terrasaur/utils/AppVersion.java | 19 +- src/main/java/terrasaur/utils/Binary16.java | 132 +- .../java/terrasaur/utils/BinaryUtils.java | 130 +- .../java/terrasaur/utils/ByteSwapper.java | 236 +- src/main/java/terrasaur/utils/CellInfo.java | 490 ++- src/main/java/terrasaur/utils/DTMHeader.java | 5 +- src/main/java/terrasaur/utils/FitPlane.java | 205 +- src/main/java/terrasaur/utils/FitSurface.java | 168 +- .../java/terrasaur/utils/GMTGridUtil.java | 1205 +++--- src/main/java/terrasaur/utils/ICQUtils.java | 12 +- .../java/terrasaur/utils/JCommanderUsage.java | 611 +-- .../terrasaur/utils/Log4j2Configurator.java | 301 +- .../terrasaur/utils/NativeLibraryLoader.java | 75 +- .../terrasaur/utils/PhotometricFunction.java | 138 +- .../terrasaur/utils/PolyDataStatistics.java | 863 +++-- .../java/terrasaur/utils/PolyDataUtil.java | 3373 ++++++++-------- .../java/terrasaur/utils/ProcessUtils.java | 124 +- .../terrasaur/utils/RemoveAberration.java | 262 +- .../java/terrasaur/utils/ResourceUtils.java | 201 +- .../terrasaur/utils/SBMTEllipseRecord.java | 157 +- src/main/java/terrasaur/utils/SPICEUtil.java | 261 +- src/main/java/terrasaur/utils/StringUtil.java | 701 ++-- src/main/java/terrasaur/utils/SumFile.java | 841 ++-- src/main/java/terrasaur/utils/Tilts.java | 55 +- .../terrasaur/utils/VectorStatistics.java | 226 +- .../java/terrasaur/utils/VectorUtils.java | 46 +- src/main/java/terrasaur/utils/XYGrid.java | 381 +- .../utils/batch/BatchSubmitFactory.java | 48 +- .../terrasaur/utils/batch/BatchSubmitI.java | 103 +- .../utils/batch/BatchSubmitLocal.java | 338 +- .../utils/batch/BatchSubmitOpenGrid.java | 661 ++-- .../java/terrasaur/utils/batch/BatchType.java | 205 +- .../java/terrasaur/utils/batch/GridType.java | 132 +- .../terrasaur/utils/batch/package-info.java | 2 +- .../utils/gravity/GravityOptions.java | 183 +- .../utils/gravity/GravityResult.java | 134 +- .../terrasaur/utils/gravity/GravityUtils.java | 126 +- .../utils/lidar/LidarTransformation.java | 209 +- .../terrasaur/utils/math/MathConversions.java | 272 +- .../terrasaur/utils/math/RotationUtils.java | 645 ++-- .../terrasaur/utils/mesh/TriangularFacet.java | 292 +- .../terrasaur/utils/mesh/TriangularMesh.java | 1493 ++++--- .../terrasaur/utils/octree/BoundingBox.java | 156 +- .../java/terrasaur/utils/octree/Octree.java | 362 +- .../utils/saaPlotLib/canvas/ActivityPlot.java | 175 +- .../utils/saaPlotLib/canvas/AreaPlot.java | 168 +- .../saaPlotLib/canvas/DiscreteDataPlot.java | 654 ++-- .../utils/saaPlotLib/canvas/MapPlot.java | 882 +++-- .../utils/saaPlotLib/canvas/PlotCanvas.java | 998 +++-- .../utils/saaPlotLib/canvas/PolarPlot.java | 747 ++-- .../canvas/RectangularPlotCanvas.java | 84 +- .../utils/saaPlotLib/canvas/axis/Axis.java | 293 +- .../utils/saaPlotLib/canvas/axis/AxisR.java | 19 +- .../saaPlotLib/canvas/axis/AxisRange.java | 448 ++- .../saaPlotLib/canvas/axis/AxisTheta.java | 19 +- .../utils/saaPlotLib/canvas/axis/AxisX.java | 24 +- .../utils/saaPlotLib/canvas/axis/AxisY.java | 29 +- .../saaPlotLib/canvas/axis/TickMarks.java | 212 +- .../saaPlotLib/canvas/axis/UTCAxisX.java | 303 +- .../canvas/projection/Projection.java | 231 +- .../projection/ProjectionMollweide.java | 122 +- .../projection/ProjectionOrthographic.java | 223 +- .../projection/ProjectionRectangular.java | 131 +- .../saaPlotLib/canvas/symbol/Asterisk.java | 29 +- .../saaPlotLib/canvas/symbol/Circle.java | 24 +- .../utils/saaPlotLib/canvas/symbol/Cross.java | 29 +- .../saaPlotLib/canvas/symbol/Square.java | 38 +- .../saaPlotLib/canvas/symbol/Symbol.java | 100 +- .../saaPlotLib/canvas/symbol/Triangle.java | 38 +- .../utils/saaPlotLib/colorMaps/ColorBar.java | 17 +- .../utils/saaPlotLib/colorMaps/ColorRamp.java | 595 +-- .../colorMaps/DivergentColorRamp.java | 103 +- .../colorMaps/tables/ColorTable.java | 25 +- .../saaPlotLib/colorMaps/tables/Colorcet.java | 1427 ++++--- .../saaPlotLib/colorMaps/tables/IDL.java | 79 +- .../saaPlotLib/colorMaps/tables/MATLAB.java | 77 +- .../colorMaps/tables/Matplotlib.java | 76 +- .../tables/ScientificColourMaps6.java | 470 ++- .../utils/saaPlotLib/config/PlotConfig.java | 242 +- .../utils/saaPlotLib/data/Activity.java | 36 +- .../utils/saaPlotLib/data/ActivitySet.java | 85 +- .../utils/saaPlotLib/data/Annotation.java | 37 +- .../utils/saaPlotLib/data/Annotations.java | 72 +- .../saaPlotLib/data/DiscreteDataSet.java | 333 +- .../saaPlotLib/data/HistogramDataSet.java | 122 +- .../utils/saaPlotLib/data/Point3D.java | 3 +- .../utils/saaPlotLib/data/Point4D.java | 237 +- .../utils/saaPlotLib/data/PointList.java | 621 +-- .../saaPlotLib/demo/ActivityPlotDemo.java | 181 +- .../utils/saaPlotLib/demo/AreaPlotDemo.java | 232 +- .../utils/saaPlotLib/demo/BarPlotDemo.java | 61 +- .../utils/saaPlotLib/demo/CSVPlotDemo.java | 92 +- .../utils/saaPlotLib/demo/ColorRampDemo.java | 105 +- .../utils/saaPlotLib/demo/LinePlotDemo.java | 266 +- .../utils/saaPlotLib/demo/MapPlotDemo.java | 431 +-- .../utils/saaPlotLib/demo/MultiPlotDemo.java | 43 +- .../utils/saaPlotLib/demo/PolarPlotDemo.java | 92 +- .../utils/saaPlotLib/demo/SandPlotDemo.java | 95 +- .../saaPlotLib/demo/ScatterPlotDemo.java | 57 +- .../utils/saaPlotLib/util/Keyword.java | 8 +- .../utils/saaPlotLib/util/LegendEntry.java | 28 +- .../utils/saaPlotLib/util/PlotUtils.java | 344 +- .../saaPlotLib/util/StringFunctions.java | 68 +- .../utils/saaPlotLib/util/StringUtils.java | 130 +- .../terrasaur/utils/spice/SpiceBundle.java | 1175 +++--- .../terrasaur/utils/spice/SpkCodeBinder.java | 68 +- .../tessellation/AbrateTessellation.java | 1074 +++--- .../utils/tessellation/FibonacciSphere.java | 518 ++- .../tessellation/SphericalTessellation.java | 98 +- .../tessellation/StereographicProjection.java | 183 +- .../java/terrasaur/utils/xml/AsciiFile.java | 189 +- .../smallBodyModel/SBMTStructureTest.java | 17 +- .../java/terrasaur/utils/Binary16Test.java | 21 +- .../java/terrasaur/utils/FitSurfaceTest.java | 231 +- .../terrasaur/utils/RemoveAberrationTest.java | 188 +- .../terrasaur/utils/ResourceUtilsTest.java | 38 +- .../java/terrasaur/utils/SumFileTest.java | 98 +- .../utils/batch/TestBatchSubmit.java | 64 +- .../utils/lidar/LidarTransformationTest.java | 69 +- .../utils/math/MathConversionsTest.java | 401 +- .../utils/math/RotationUtilsTest.java | 255 +- .../canvas/DiscreteDataPlotTest.java | 41 +- .../saaPlotLib/canvas/axis/AxisRangeTest.java | 93 +- .../saaPlotLib/canvas/axis/TickMarksTest.java | 40 +- .../saaPlotLib/canvas/axis/UTCAxisTest.java | 106 +- .../colorMaps/DivergentColorRampTest.java | 34 +- .../utils/saaPlotLib/data/PointListTest.java | 3 +- .../utils/saaPlotLib/util/PlotUtilsTest.java | 1 - 210 files changed, 36128 insertions(+), 36845 deletions(-) diff --git a/src/main/java/terrasaur/altwg/pipeline/ALTWGProductNamer.java b/src/main/java/terrasaur/altwg/pipeline/ALTWGProductNamer.java index eb1289d..9200d3c 100644 --- a/src/main/java/terrasaur/altwg/pipeline/ALTWGProductNamer.java +++ b/src/main/java/terrasaur/altwg/pipeline/ALTWGProductNamer.java @@ -29,319 +29,314 @@ import terrasaur.utils.StringUtil; public class ALTWGProductNamer implements ProductNamer { - public ALTWGProductNamer() { - super(); - } - - /** - * Parse the productName and return the portion of the name corresponding to a given field. Fields - * are assumed separated by "_" in the filename. - * - * @param productName - * @param fieldNum - * @return - */ - @Override - public String getNameFrag(String productName, int fieldNum) { - - String[] fields = productName.split("_"); - String returnField = "ERROR"; - if (fieldNum > fields.length) { - System.out.println( - "ERROR, field:" + fieldNum + " requested is beyond the number of fields found."); - System.out.println("returning:" + returnField); - } else { - returnField = fields[fieldNum]; - } - return returnField; - } - - @Override - public String productbaseName( - FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - - String gsd = "gsd"; - String dataSrc = "dataSrc"; - String productType = altwgProduct.getFileFrag(); - String prodVer = getVersion(hdrBuilder); - - // extract ground sample distance. gsdD is in mm! - double gsdD = gsdFromHdr(hdrBuilder); - - int gsdI = (int) Math.round(gsdD); - String fileUnits = "mm"; - gsd = String.format("%05d", gsdI) + fileUnits; - - // System.out.println("gsd:" + gsd); - // System.out.println("file units:" + fileUnits); - - HeaderTag key = HeaderTag.DATASRC; - if (hdrBuilder.containsKey(key)) { - dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); - - // check whether dataSrc needs to be modified - dataSrc = HeaderTag.getSDP(dataSrc); - // data source should only be 3 chars long - if (dataSrc.length() > 3) { - dataSrc = dataSrc.substring(0, 3); - } + public ALTWGProductNamer() { + super(); } - key = HeaderTag.CLON; - String cLon = null; - if (hdrBuilder.containsKey(key)) { - cLon = hdrBuilder.getCard(key).getValue(); - } - if (cLon == null) { - if (isGlobal) { - // set center longitude to 0.0 if value not parsed and this is a global product - cLon = "0.0"; - } else { - String errMesg = "ERROR! Could not parse CLON from fits header!"; - throw new RuntimeException(errMesg); - } - } - // System.out.println("clon:" + cLon); - key = HeaderTag.CLAT; - String cLat = null; - if (hdrBuilder.containsKey(key)) { - cLat = hdrBuilder.getCard(key).getValue(); - } - if (cLat == null) { - if (isGlobal) { - // set center latitude to 0.0 if value not parsed and this is a global product - cLat = "0.0"; - } else { - String errMesg = "ERROR! Could not parse CLAT from fits header!"; - throw new RuntimeException(errMesg); - } - } - // System.out.println("clat" + cLat); + /** + * Parse the productName and return the portion of the name corresponding to a given field. Fields + * are assumed separated by "_" in the filename. + * + * @param productName + * @param fieldNum + * @return + */ + @Override + public String getNameFrag(String productName, int fieldNum) { - String region = "l"; - if (isGlobal) { - region = "g"; - } - - String clahLon = ALTWGProductNamer.clahLon(cLat, cLon); - - // pds likes having filenames all in the same case, so chose lowercase - String outFile = - ALTWGProductNamer.altwgBaseName(region, gsd, dataSrc, productType, clahLon, prodVer); - return outFile; - } - - /** - * Retrieve the product version string. Returns initial default value if product version keyword - * not found in builder. - * - * @param hdrBuilder - */ - @Override - public String getVersion(FitsHdrBuilder hdrBuilder) { - String prodVer = "prodVer"; - - // note: this has been changed to MAP_VER in the SIS - HeaderTag key = HeaderTag.MAP_VER; - // key = HeaderTag.PRODVERS; - if (hdrBuilder.containsKey(key)) { - prodVer = hdrBuilder.getCard(key).getValue(); - prodVer = prodVer.replaceAll("\\.", ""); - } - - return prodVer; - } - - // Given the fields return the altwg PDS base name - public static String altwgBaseName( - String region, String gsd, String dataSource, String desc, String lahLon, String version) { - - StringBuilder sb = new StringBuilder(); - String delim = "_"; - sb.append(region); - sb.append(delim); - sb.append(gsd); - sb.append(delim); - - // data source should only be 3 characters long - if (dataSource.length() > 3) { - System.out.println("WARNING! dataSource:" + dataSource + " longer than 3 chars!"); - dataSource = dataSource.substring(0, 3); - System.out.println( - "Will set data source to:" - + dataSource - + " but" - + " this might NOT conform to the ALTWG naming convention!"); - } - sb.append(dataSource); - sb.append(delim); - sb.append(desc); - sb.append(delim); - sb.append(lahLon); - sb.append(delim); - sb.append("v"); - - // remove '.' from version string - version = version.replaceAll("\\.", ""); - sb.append(version); - - // pds likes having filenames all in the same case, so chose lowercase - String outFile = sb.toString().toLowerCase(); - - return outFile; - } - - /** - * Parse center lat, lon strings and return the formatted clahLon portion of the PDS filename. - * - * @param clat - * @param clon - * @return - */ - public static String clahLon(String clat, String clon) { - - // String cLon = ""; - - // remove all whitespace that may exist in the strings - clat = clat.replaceAll("\\s+", ""); - clon = clon.replaceAll("\\s+", ""); - - double cLonD = StringUtil.parseSafeD(clon); - double cLatD = StringUtil.parseSafeD(clat); - - String clahLon = clahLon(cLatD, cLonD); - return clahLon; - } - - public static String clahLon(double cLatD, double cLonD) { - - String cLon = ""; - - if (cLonD == Double.NaN) { - - // unable to parse center longitude using normal method (see V in getProductCards()) - cLon = "xxxxxx"; - - } else { - - if (cLonD < 0) { - // transform to 0-360 - cLonD = cLonD + 360D; - } - // format double to 2 significant digits - cLon = String.format("%06.2f", cLonD); - } - - // remove decimal point - cLon = cLon.replace(".", ""); - - String cLat = ""; - - // System.out.println("cLatD:" + Double.toString(cLatD)); - if (cLatD == Double.NaN) { - - // unable to parse center latitude - cLat = "xxxxxx"; - - } else { - - double tol = 0.0101D; - - // determine whether latitude is within tolerance of its rounded value. - // if so then use rounded value - double roundValue = Math.round(cLatD); - double diffTol = Math.abs(roundValue - cLatD); - if (diffTol < tol) { - cLatD = roundValue; - } - String hemiSphere = (cLatD >= 0) ? "N" : "S"; - - if (cLatD < 0) { - // remove negative sign if in southern hemisphere - cLatD = cLatD * -1.0D; - } - // format cLat to 2 significant digits - cLat = String.format("%05.2f", cLatD); - cLat = cLat.replace(".", ""); - - // trim to length 4. - cLat = cLat.substring(0, Math.min(cLat.length(), 4)); - cLat = cLat + hemiSphere; - } - - String clahLon = cLat + cLon; - return clahLon; - } - - /** - * return GSD parsed from FitsHdrBuilder. Returns 0 if valid GSD could not be parsed. GSD is in - * mm. - * - * @param hdrBuilder - * @return - */ - @Override - public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { - - String gsd = "gsd"; - double gsdD = Double.NaN; - - // extract ground sample distance using GSD first - HeaderTag key = HeaderTag.GSD; - if (hdrBuilder.containsKey(key)) { - gsd = hdrBuilder.getCard(key).getValue(); - gsdD = StringUtil.parseSafeD(gsd); - if (gsdD < 0D) { - // keyword value not initialized - gsdD = Double.NaN; - System.out.println("WARNING! keyword GSD not set!"); - } - } else { - System.out.println("could not find " + key.toString() + " to parse GSD from."); - } - if (Double.isNaN(gsdD)) { - // could not parse GSD into valid number, try GSDI - key = HeaderTag.GSDI; - if (hdrBuilder.containsKey(key)) { - gsdD = StringUtil.parseSafeD(hdrBuilder.getCard(key).getValue()); - if (gsdD < 0D) { - // keyword value not initialized - gsdD = Double.NaN; - System.out.println("WARNING! keyword GSDI not set!"); + String[] fields = productName.split("_"); + String returnField = "ERROR"; + if (fieldNum > fields.length) { + System.out.println("ERROR, field:" + fieldNum + " requested is beyond the number of fields found."); + System.out.println("returning:" + returnField); + } else { + returnField = fields[fieldNum]; } - } else { - System.out.println("could not find " + key.toString() + " to parse GSD from."); - } - if (Double.isNaN(gsdD)) { - // still cannot parse gsd. Set to -999 - System.out.println( - "WARNING: No valid values of GSD or GSDI could be parsed from fits header!"); - System.out.println("Setting gsd = -999"); - gsdD = -999D; - } + return returnField; } - if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + @Override + public String productbaseName(FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - // mandated to use mm! change the units - gsdD = gsdD * 10.0D; + String gsd = "gsd"; + String dataSrc = "dataSrc"; + String productType = altwgProduct.getFileFrag(); + String prodVer = getVersion(hdrBuilder); + + // extract ground sample distance. gsdD is in mm! + double gsdD = gsdFromHdr(hdrBuilder); + + int gsdI = (int) Math.round(gsdD); + String fileUnits = "mm"; + gsd = String.format("%05d", gsdI) + fileUnits; + + // System.out.println("gsd:" + gsd); + // System.out.println("file units:" + fileUnits); + + HeaderTag key = HeaderTag.DATASRC; + if (hdrBuilder.containsKey(key)) { + dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); + + // check whether dataSrc needs to be modified + dataSrc = HeaderTag.getSDP(dataSrc); + // data source should only be 3 chars long + if (dataSrc.length() > 3) { + dataSrc = dataSrc.substring(0, 3); + } + } + + key = HeaderTag.CLON; + String cLon = null; + if (hdrBuilder.containsKey(key)) { + cLon = hdrBuilder.getCard(key).getValue(); + } + if (cLon == null) { + if (isGlobal) { + // set center longitude to 0.0 if value not parsed and this is a global product + cLon = "0.0"; + } else { + String errMesg = "ERROR! Could not parse CLON from fits header!"; + throw new RuntimeException(errMesg); + } + } + // System.out.println("clon:" + cLon); + key = HeaderTag.CLAT; + String cLat = null; + if (hdrBuilder.containsKey(key)) { + cLat = hdrBuilder.getCard(key).getValue(); + } + if (cLat == null) { + if (isGlobal) { + // set center latitude to 0.0 if value not parsed and this is a global product + cLat = "0.0"; + } else { + String errMesg = "ERROR! Could not parse CLAT from fits header!"; + throw new RuntimeException(errMesg); + } + } + // System.out.println("clat" + cLat); + + String region = "l"; + if (isGlobal) { + region = "g"; + } + + String clahLon = ALTWGProductNamer.clahLon(cLat, cLon); + + // pds likes having filenames all in the same case, so chose lowercase + String outFile = ALTWGProductNamer.altwgBaseName(region, gsd, dataSrc, productType, clahLon, prodVer); + return outFile; } - return gsdD; - } + /** + * Retrieve the product version string. Returns initial default value if product version keyword + * not found in builder. + * + * @param hdrBuilder + */ + @Override + public String getVersion(FitsHdrBuilder hdrBuilder) { + String prodVer = "prodVer"; - @Override - public NameConvention getNameConvention() { - return NameConvention.ALTPRODUCT; - } + // note: this has been changed to MAP_VER in the SIS + HeaderTag key = HeaderTag.MAP_VER; + // key = HeaderTag.PRODVERS; + if (hdrBuilder.containsKey(key)) { + prodVer = hdrBuilder.getCard(key).getValue(); + prodVer = prodVer.replaceAll("\\.", ""); + } - @Override - /** Parse the filename using the ALTWGProduct naming convention and return the GSD value. */ - public double gsdFromFilename(String filename) { - String[] splitStr = filename.split("_"); - // GSD is second element - String gsd = splitStr[1]; - gsd = gsd.replace("mm", ""); - return StringUtil.parseSafeD(gsd); - } + return prodVer; + } + + // Given the fields return the altwg PDS base name + public static String altwgBaseName( + String region, String gsd, String dataSource, String desc, String lahLon, String version) { + + StringBuilder sb = new StringBuilder(); + String delim = "_"; + sb.append(region); + sb.append(delim); + sb.append(gsd); + sb.append(delim); + + // data source should only be 3 characters long + if (dataSource.length() > 3) { + System.out.println("WARNING! dataSource:" + dataSource + " longer than 3 chars!"); + dataSource = dataSource.substring(0, 3); + System.out.println("Will set data source to:" + + dataSource + + " but" + + " this might NOT conform to the ALTWG naming convention!"); + } + sb.append(dataSource); + sb.append(delim); + sb.append(desc); + sb.append(delim); + sb.append(lahLon); + sb.append(delim); + sb.append("v"); + + // remove '.' from version string + version = version.replaceAll("\\.", ""); + sb.append(version); + + // pds likes having filenames all in the same case, so chose lowercase + String outFile = sb.toString().toLowerCase(); + + return outFile; + } + + /** + * Parse center lat, lon strings and return the formatted clahLon portion of the PDS filename. + * + * @param clat + * @param clon + * @return + */ + public static String clahLon(String clat, String clon) { + + // String cLon = ""; + + // remove all whitespace that may exist in the strings + clat = clat.replaceAll("\\s+", ""); + clon = clon.replaceAll("\\s+", ""); + + double cLonD = StringUtil.parseSafeD(clon); + double cLatD = StringUtil.parseSafeD(clat); + + String clahLon = clahLon(cLatD, cLonD); + return clahLon; + } + + public static String clahLon(double cLatD, double cLonD) { + + String cLon = ""; + + if (cLonD == Double.NaN) { + + // unable to parse center longitude using normal method (see V in getProductCards()) + cLon = "xxxxxx"; + + } else { + + if (cLonD < 0) { + // transform to 0-360 + cLonD = cLonD + 360D; + } + // format double to 2 significant digits + cLon = String.format("%06.2f", cLonD); + } + + // remove decimal point + cLon = cLon.replace(".", ""); + + String cLat = ""; + + // System.out.println("cLatD:" + Double.toString(cLatD)); + if (cLatD == Double.NaN) { + + // unable to parse center latitude + cLat = "xxxxxx"; + + } else { + + double tol = 0.0101D; + + // determine whether latitude is within tolerance of its rounded value. + // if so then use rounded value + double roundValue = Math.round(cLatD); + double diffTol = Math.abs(roundValue - cLatD); + if (diffTol < tol) { + cLatD = roundValue; + } + String hemiSphere = (cLatD >= 0) ? "N" : "S"; + + if (cLatD < 0) { + // remove negative sign if in southern hemisphere + cLatD = cLatD * -1.0D; + } + // format cLat to 2 significant digits + cLat = String.format("%05.2f", cLatD); + cLat = cLat.replace(".", ""); + + // trim to length 4. + cLat = cLat.substring(0, Math.min(cLat.length(), 4)); + cLat = cLat + hemiSphere; + } + + String clahLon = cLat + cLon; + return clahLon; + } + + /** + * return GSD parsed from FitsHdrBuilder. Returns 0 if valid GSD could not be parsed. GSD is in + * mm. + * + * @param hdrBuilder + * @return + */ + @Override + public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { + + String gsd = "gsd"; + double gsdD = Double.NaN; + + // extract ground sample distance using GSD first + HeaderTag key = HeaderTag.GSD; + if (hdrBuilder.containsKey(key)) { + gsd = hdrBuilder.getCard(key).getValue(); + gsdD = StringUtil.parseSafeD(gsd); + if (gsdD < 0D) { + // keyword value not initialized + gsdD = Double.NaN; + System.out.println("WARNING! keyword GSD not set!"); + } + } else { + System.out.println("could not find " + key.toString() + " to parse GSD from."); + } + if (Double.isNaN(gsdD)) { + // could not parse GSD into valid number, try GSDI + key = HeaderTag.GSDI; + if (hdrBuilder.containsKey(key)) { + gsdD = StringUtil.parseSafeD(hdrBuilder.getCard(key).getValue()); + if (gsdD < 0D) { + // keyword value not initialized + gsdD = Double.NaN; + System.out.println("WARNING! keyword GSDI not set!"); + } + } else { + System.out.println("could not find " + key.toString() + " to parse GSD from."); + } + if (Double.isNaN(gsdD)) { + // still cannot parse gsd. Set to -999 + System.out.println("WARNING: No valid values of GSD or GSDI could be parsed from fits header!"); + System.out.println("Setting gsd = -999"); + gsdD = -999D; + } + } + + if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + + // mandated to use mm! change the units + gsdD = gsdD * 10.0D; + } + + return gsdD; + } + + @Override + public NameConvention getNameConvention() { + return NameConvention.ALTPRODUCT; + } + + @Override + /** Parse the filename using the ALTWGProduct naming convention and return the GSD value. */ + public double gsdFromFilename(String filename) { + String[] splitStr = filename.split("_"); + // GSD is second element + String gsd = splitStr[1]; + gsd = gsd.replace("mm", ""); + return StringUtil.parseSafeD(gsd); + } } diff --git a/src/main/java/terrasaur/altwg/pipeline/AltwgMLNNamer.java b/src/main/java/terrasaur/altwg/pipeline/AltwgMLNNamer.java index c544cd8..9863d19 100644 --- a/src/main/java/terrasaur/altwg/pipeline/AltwgMLNNamer.java +++ b/src/main/java/terrasaur/altwg/pipeline/AltwgMLNNamer.java @@ -29,184 +29,182 @@ import terrasaur.utils.StringUtil; public class AltwgMLNNamer implements ProductNamer { - public AltwgMLNNamer() { - super(); - } - - @Override - public String getNameFrag(String productName, int fieldNum) { - - String nameFrag = ""; - - return nameFrag; - } - - /** - * ALTWG MLN naming convention applies only to one productType - the ALTWG NFT-MLN. - * - * @param hdrBuilder - contains values that are used to create the MLN according to naming - * convention. - * @param altwgProduct - N/A. Included here as part of the interface structure. - * @param isGlobal - N/A. Included here as part of the interface structure. - */ - @Override - public String productbaseName( - FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - - // initialize string fragments for NFT name. This will help identify - // which string fragments have not been updated by the method. - String gsd = "gsd"; - String dataSrc = "dataSrc"; - String dataSrcfile = "dataSrcFile"; - String productType = "nftdtm"; - String cLon = "cLon"; - String cLat = "cLat"; - String prodVer = "prodVer"; - String productID = "prodID"; - - // find relevant information in the hdrBuilder map. - double gsdD = gsdFromHdr(hdrBuilder); - int gsdI = (int) Math.round(gsdD); - String fileUnits = "mm"; - gsd = String.format("%05d", gsdI) + fileUnits; - // HeaderTag key = HeaderTag.GSD; - // if (hdrBuilder.containsKey(key)) { - // gsd = hdrBuilder.getCard(key).getValue(); - // - // double gsdD = Double.parseDouble(gsd); - // String fileUnits = ""; - // if (hdrBuilder.getCard(key).getComment().contains("[mm]")) { - // fileUnits = "mm"; - // } else if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { - // - // // mandated to use mm! change the units - // gsdD = gsdD * 10.0D; - // gsdI = (int) Math.round(gsdD); - // } - // - // System.out.println("gsd:" + gsd); - // System.out.println("file units:" + fileUnits); - // - // } - - HeaderTag key = HeaderTag.DATASRC; - if (hdrBuilder.containsKey(key)) { - dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); - // data source should only be 3 chars long - if (dataSrc.length() > 3) { - dataSrc = dataSrc.substring(0, 3); - } + public AltwgMLNNamer() { + super(); } - key = HeaderTag.CLON; - if (hdrBuilder.containsKey(key)) { - cLon = hdrBuilder.getCard(key).getValue(); + @Override + public String getNameFrag(String productName, int fieldNum) { + + String nameFrag = ""; + + return nameFrag; } - key = HeaderTag.CLAT; - if (hdrBuilder.containsKey(key)) { - cLat = hdrBuilder.getCard(key).getValue(); - } - - key = HeaderTag.DATASRCF; - if (hdrBuilder.containsKey(key)) { - dataSrcfile = hdrBuilder.getCard(key).getValue(); - } - - key = HeaderTag.PRODVERS; - if (hdrBuilder.containsKey(key)) { - prodVer = hdrBuilder.getCard(key).getValue(); - prodVer = prodVer.replaceAll("\\.", ""); - } - - // hardcode region to local - String region = "l"; - - StringBuilder sb = new StringBuilder(); - String delim = "_"; - sb.append(region); - sb.append(delim); - sb.append(gsd); - sb.append(delim); - sb.append(dataSrc); - sb.append(delim); - sb.append(productType); - sb.append(delim); - - /* - * determine product ID. For OLA it is the center lat,lon For SPC it is the NFT feature id, - * which is assumed to be the first 5 chars in DATASRC + /** + * ALTWG MLN naming convention applies only to one productType - the ALTWG NFT-MLN. + * + * @param hdrBuilder - contains values that are used to create the MLN according to naming + * convention. + * @param altwgProduct - N/A. Included here as part of the interface structure. + * @param isGlobal - N/A. Included here as part of the interface structure. */ - if (dataSrc.contains("ola")) { + @Override + public String productbaseName(FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - // follow ALTWG product naming convention for center lat, lon - productID = ALTWGProductNamer.clahLon(cLat, cLon); - } else { - productID = dataSrcfile.substring(0, 5); - } - sb.append(productID); - sb.append(delim); - sb.append("v"); - sb.append(prodVer); + // initialize string fragments for NFT name. This will help identify + // which string fragments have not been updated by the method. + String gsd = "gsd"; + String dataSrc = "dataSrc"; + String dataSrcfile = "dataSrcFile"; + String productType = "nftdtm"; + String cLon = "cLon"; + String cLat = "cLat"; + String prodVer = "prodVer"; + String productID = "prodID"; - return sb.toString().toLowerCase(); - } + // find relevant information in the hdrBuilder map. + double gsdD = gsdFromHdr(hdrBuilder); + int gsdI = (int) Math.round(gsdD); + String fileUnits = "mm"; + gsd = String.format("%05d", gsdI) + fileUnits; + // HeaderTag key = HeaderTag.GSD; + // if (hdrBuilder.containsKey(key)) { + // gsd = hdrBuilder.getCard(key).getValue(); + // + // double gsdD = Double.parseDouble(gsd); + // String fileUnits = ""; + // if (hdrBuilder.getCard(key).getComment().contains("[mm]")) { + // fileUnits = "mm"; + // } else if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + // + // // mandated to use mm! change the units + // gsdD = gsdD * 10.0D; + // gsdI = (int) Math.round(gsdD); + // } + // + // System.out.println("gsd:" + gsd); + // System.out.println("file units:" + fileUnits); + // + // } - @Override - public String getVersion(FitsHdrBuilder hdrBuilder) { + HeaderTag key = HeaderTag.DATASRC; + if (hdrBuilder.containsKey(key)) { + dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); + // data source should only be 3 chars long + if (dataSrc.length() > 3) { + dataSrc = dataSrc.substring(0, 3); + } + } - String version = ""; + key = HeaderTag.CLON; + if (hdrBuilder.containsKey(key)) { + cLon = hdrBuilder.getCard(key).getValue(); + } - return version; - } + key = HeaderTag.CLAT; + if (hdrBuilder.containsKey(key)) { + cLat = hdrBuilder.getCard(key).getValue(); + } - /** - * Extract ground sample distance from FitsHdrBuilder. GSD is needed as part of naming convention. - * GSD in units of mm. - */ - @Override - public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { + key = HeaderTag.DATASRCF; + if (hdrBuilder.containsKey(key)) { + dataSrcfile = hdrBuilder.getCard(key).getValue(); + } - // find relevant information in the hdrBuilder map. - double gsdD = Double.NaN; - HeaderTag key = HeaderTag.GSD; - if (hdrBuilder.containsKey(key)) { - String gsd = hdrBuilder.getCard(key).getValue(); + key = HeaderTag.PRODVERS; + if (hdrBuilder.containsKey(key)) { + prodVer = hdrBuilder.getCard(key).getValue(); + prodVer = prodVer.replaceAll("\\.", ""); + } - gsdD = Double.parseDouble(gsd); - String fileUnits = ""; - if (hdrBuilder.getCard(key).getComment().contains("[mm]")) { - fileUnits = "mm"; - } else if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + // hardcode region to local + String region = "l"; - // mandated to use mm! change the units - gsdD = gsdD * 10.0D; - fileUnits = "mm"; - } - System.out.println("gsd:" + gsd); - System.out.println("file units:" + fileUnits); + StringBuilder sb = new StringBuilder(); + String delim = "_"; + sb.append(region); + sb.append(delim); + sb.append(gsd); + sb.append(delim); + sb.append(dataSrc); + sb.append(delim); + sb.append(productType); + sb.append(delim); - } else { - String errMesg = - "ERROR! Could not find keyword:" + HeaderTag.GSD.toString() + " in hdrBuilder"; - throw new RuntimeException(errMesg); + /* + * determine product ID. For OLA it is the center lat,lon For SPC it is the NFT feature id, + * which is assumed to be the first 5 chars in DATASRC + */ + if (dataSrc.contains("ola")) { + + // follow ALTWG product naming convention for center lat, lon + productID = ALTWGProductNamer.clahLon(cLat, cLon); + } else { + productID = dataSrcfile.substring(0, 5); + } + sb.append(productID); + sb.append(delim); + sb.append("v"); + sb.append(prodVer); + + return sb.toString().toLowerCase(); } - return gsdD; - } + @Override + public String getVersion(FitsHdrBuilder hdrBuilder) { - @Override - public NameConvention getNameConvention() { - return NameConvention.ALTNFTMLN; - } + String version = ""; - @Override - /** Parse the filename using the ALTWG MLN naming convention and return the GSD value. */ - public double gsdFromFilename(String filename) { - String[] splitStr = filename.split("_"); - // GSD is second element - String gsd = splitStr[1]; - gsd = gsd.replace("mm", ""); - return StringUtil.parseSafeD(gsd); - } + return version; + } + + /** + * Extract ground sample distance from FitsHdrBuilder. GSD is needed as part of naming convention. + * GSD in units of mm. + */ + @Override + public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { + + // find relevant information in the hdrBuilder map. + double gsdD = Double.NaN; + HeaderTag key = HeaderTag.GSD; + if (hdrBuilder.containsKey(key)) { + String gsd = hdrBuilder.getCard(key).getValue(); + + gsdD = Double.parseDouble(gsd); + String fileUnits = ""; + if (hdrBuilder.getCard(key).getComment().contains("[mm]")) { + fileUnits = "mm"; + } else if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + + // mandated to use mm! change the units + gsdD = gsdD * 10.0D; + fileUnits = "mm"; + } + System.out.println("gsd:" + gsd); + System.out.println("file units:" + fileUnits); + + } else { + String errMesg = "ERROR! Could not find keyword:" + HeaderTag.GSD.toString() + " in hdrBuilder"; + throw new RuntimeException(errMesg); + } + + return gsdD; + } + + @Override + public NameConvention getNameConvention() { + return NameConvention.ALTNFTMLN; + } + + @Override + /** Parse the filename using the ALTWG MLN naming convention and return the GSD value. */ + public double gsdFromFilename(String filename) { + String[] splitStr = filename.split("_"); + // GSD is second element + String gsd = splitStr[1]; + gsd = gsd.replace("mm", ""); + return StringUtil.parseSafeD(gsd); + } } diff --git a/src/main/java/terrasaur/altwg/pipeline/DartNamer.java b/src/main/java/terrasaur/altwg/pipeline/DartNamer.java index 1640359..758a058 100644 --- a/src/main/java/terrasaur/altwg/pipeline/DartNamer.java +++ b/src/main/java/terrasaur/altwg/pipeline/DartNamer.java @@ -36,291 +36,287 @@ import terrasaur.utils.StringUtil; */ public class DartNamer implements ProductNamer { - public static String getBaseName(Map nameFragments) { + public static String getBaseName(Map nameFragments) { - // check to see if the map contains all the fragments needed. Throw runtimeexception if it - // doesn't - validateMap(nameFragments); + // check to see if the map contains all the fragments needed. Throw runtimeexception if it + // doesn't + validateMap(nameFragments); - StringBuilder sb = new StringBuilder(); - String delim = "_"; - sb.append(nameFragments.get(NameFields.REGION)); - sb.append(delim); - sb.append(nameFragments.get(NameFields.GSD)); - sb.append(delim); + StringBuilder sb = new StringBuilder(); + String delim = "_"; + sb.append(nameFragments.get(NameFields.REGION)); + sb.append(delim); + sb.append(nameFragments.get(NameFields.GSD)); + sb.append(delim); - // data source should only be 3 characters long - String dataSource = nameFragments.get(NameFields.DATASRC); - if (dataSource.length() > 3) { - System.out.println("WARNING! dataSource:" + dataSource + " longer than 3 chars!"); - dataSource = dataSource.substring(0, 3); - System.out.println( - "Will set data source to:" - + dataSource - + " but" - + " this might NOT conform to the ALTWG naming convention!"); - } - sb.append(dataSource); - sb.append(delim); - - sb.append(nameFragments.get(NameFields.DATATYPE)); - sb.append(delim); - sb.append(nameFragments.get(NameFields.TBODY)); - sb.append(delim); - sb.append(nameFragments.get(NameFields.CLATLON)); - sb.append(delim); - sb.append("v"); - - // remove '.' from version string - String version = nameFragments.get(NameFields.VERSION); - version = version.replaceAll("\\.", ""); - sb.append(version); - - // pds likes having filenames all in the same case, so chose lowercase - String outFile = sb.toString().toLowerCase(); - - return outFile; - } - - /** - * Parse the productName and return the portion of the name corresponding to a given field. Fields - * are assumed separated by "_" in the filename. - * - * @param productName - * @param fieldNum - * @return - */ - @Override - public String getNameFrag(String productName, int fieldNum) { - - String[] fields = productName.split("_"); - String returnField = "ERROR"; - if (fieldNum > fields.length) { - System.out.println( - "ERROR, field:" + fieldNum + " requested is beyond the number of fields found."); - System.out.println("returning:" + returnField); - } else { - returnField = fields[fieldNum]; - } - return returnField; - } - - @Override - public String productbaseName( - FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - - String gsd = "gsd"; - String dataSrc = "dataSrc"; - - Map nameFragments = new HashMap(); - - // data type - String productType = altwgProduct.getFileFrag(); - nameFragments.put(NameFields.DATATYPE, productType); - - // product version - String prodVer = getVersion(hdrBuilder); - nameFragments.put(NameFields.VERSION, prodVer); - - // extract ground sample distance. gsdD is in mm! - double gsdD = gsdFromHdr(hdrBuilder); - - int gsdI = (int) Math.round(gsdD); - String fileUnits = "mm"; - gsd = String.format("%05d", gsdI) + fileUnits; - nameFragments.put(NameFields.GSD, gsd); - - // data source - HeaderTag key = HeaderTag.DATASRC; - if (hdrBuilder.containsKey(key)) { - dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); - // check whether dataSrc needs to be modified - dataSrc = HeaderTag.getSDP(dataSrc); - // data source should only be 3 chars long - if (dataSrc.length() > 3) { - dataSrc = dataSrc.substring(0, 3); - } - } - nameFragments.put(NameFields.DATASRC, dataSrc); - - // center lon - key = HeaderTag.CLON; - String cLon = null; - if (hdrBuilder.containsKey(key)) { - cLon = hdrBuilder.getCard(key).getValue(); - } - if (cLon == null) { - if (isGlobal) { - // set center longitude to 0.0 if value not parsed and this is a global product - cLon = "0.0"; - } else { - String errMesg = "ERROR! Could not parse CLON from fits header!"; - throw new RuntimeException(errMesg); - } - } - - // center lat - key = HeaderTag.CLAT; - String cLat = null; - if (hdrBuilder.containsKey(key)) { - cLat = hdrBuilder.getCard(key).getValue(); - } - if (cLat == null) { - if (isGlobal) { - // set center latitude to 0.0 if value not parsed and this is a global product - cLat = "0.0"; - } else { - String errMesg = "ERROR! Could not parse CLAT from fits header!"; - throw new RuntimeException(errMesg); - } - } - - String clahLon = ALTWGProductNamer.clahLon(cLat, cLon); - nameFragments.put(NameFields.CLATLON, clahLon); - - // region - String region = "l"; - if (isGlobal) { - region = "g"; - } - nameFragments.put(NameFields.REGION, region); - - // target body - key = HeaderTag.TARGET; - String tBody = "unkn"; - if (hdrBuilder.containsKey(key)) { - tBody = hdrBuilder.getCard(key).getValue(); - tBody = getBodyStFrag(tBody); - } - nameFragments.put(NameFields.TBODY, tBody); - - // pds likes having filenames all in the same case, so chose lowercase - String outFile = DartNamer.getBaseName(nameFragments); - return outFile; - } - - @Override - public String getVersion(FitsHdrBuilder hdrBuilder) { - - String prodVer = "prodVer"; - - // note: this has been changed to MAP_VER in the SIS - HeaderTag key = HeaderTag.MAP_VER; - // key = HeaderTag.PRODVERS; - if (hdrBuilder.containsKey(key)) { - prodVer = hdrBuilder.getCard(key).getValue(); - prodVer = prodVer.replaceAll("\\.", ""); - } - - return prodVer; - } - - @Override - public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { - - String gsd = "gsd"; - double gsdD = Double.NaN; - - // extract ground sample distance using GSD first - HeaderTag key = HeaderTag.GSD; - if (hdrBuilder.containsKey(key)) { - gsd = hdrBuilder.getCard(key).getValue(); - gsdD = StringUtil.parseSafeD(gsd); - if (gsdD < 0D) { - // keyword value not initialized - gsdD = Double.NaN; - System.out.println("WARNING! keyword GSD not set!"); - } - } else { - System.out.println("could not find " + key.toString() + " to parse GSD from."); - } - if (Double.isNaN(gsdD)) { - // could not parse GSD into valid number, try GSDI - key = HeaderTag.GSDI; - if (hdrBuilder.containsKey(key)) { - gsdD = StringUtil.parseSafeD(hdrBuilder.getCard(key).getValue()); - if (gsdD < 0D) { - // keyword value not initialized - gsdD = Double.NaN; - System.out.println("WARNING! keyword GSDI not set!"); + // data source should only be 3 characters long + String dataSource = nameFragments.get(NameFields.DATASRC); + if (dataSource.length() > 3) { + System.out.println("WARNING! dataSource:" + dataSource + " longer than 3 chars!"); + dataSource = dataSource.substring(0, 3); + System.out.println("Will set data source to:" + + dataSource + + " but" + + " this might NOT conform to the ALTWG naming convention!"); } - } else { - System.out.println("could not find " + key.toString() + " to parse GSD from."); - } - if (Double.isNaN(gsdD)) { - // still cannot parse gsd. Set to -999 - System.out.println( - "WARNING: No valid values of GSD or GSDI could be parsed from fits header!"); - System.out.println("Setting gsd = -999"); - gsdD = -999D; - } + sb.append(dataSource); + sb.append(delim); + + sb.append(nameFragments.get(NameFields.DATATYPE)); + sb.append(delim); + sb.append(nameFragments.get(NameFields.TBODY)); + sb.append(delim); + sb.append(nameFragments.get(NameFields.CLATLON)); + sb.append(delim); + sb.append("v"); + + // remove '.' from version string + String version = nameFragments.get(NameFields.VERSION); + version = version.replaceAll("\\.", ""); + sb.append(version); + + // pds likes having filenames all in the same case, so chose lowercase + String outFile = sb.toString().toLowerCase(); + + return outFile; } - if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + /** + * Parse the productName and return the portion of the name corresponding to a given field. Fields + * are assumed separated by "_" in the filename. + * + * @param productName + * @param fieldNum + * @return + */ + @Override + public String getNameFrag(String productName, int fieldNum) { - // mandated to use mm! change the units - gsdD = gsdD * 10.0D; + String[] fields = productName.split("_"); + String returnField = "ERROR"; + if (fieldNum > fields.length) { + System.out.println("ERROR, field:" + fieldNum + " requested is beyond the number of fields found."); + System.out.println("returning:" + returnField); + } else { + returnField = fields[fieldNum]; + } + return returnField; } - return gsdD; - } + @Override + public String productbaseName(FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal) { - @Override - public NameConvention getNameConvention() { - return NameConvention.DARTPRODUCT; - } + String gsd = "gsd"; + String dataSrc = "dataSrc"; - /** - * Parse target body string to get the proper string fragment for the target body name. - * - * @param tBody - * @return - */ - private String getBodyStFrag(String tBody) { + Map nameFragments = new HashMap(); - String returnFrag = tBody; + // data type + String productType = altwgProduct.getFileFrag(); + nameFragments.put(NameFields.DATATYPE, productType); - if (tBody.toLowerCase().contains("didy")) { - returnFrag = "didy"; - } else { - if (tBody.toLowerCase().contains("dimo")) { - returnFrag = "dimo"; - } else { - System.out.println("Could not parse target string fragment from:" + tBody); - } + // product version + String prodVer = getVersion(hdrBuilder); + nameFragments.put(NameFields.VERSION, prodVer); + + // extract ground sample distance. gsdD is in mm! + double gsdD = gsdFromHdr(hdrBuilder); + + int gsdI = (int) Math.round(gsdD); + String fileUnits = "mm"; + gsd = String.format("%05d", gsdI) + fileUnits; + nameFragments.put(NameFields.GSD, gsd); + + // data source + HeaderTag key = HeaderTag.DATASRC; + if (hdrBuilder.containsKey(key)) { + dataSrc = hdrBuilder.getCard(key).getValue().toLowerCase(); + // check whether dataSrc needs to be modified + dataSrc = HeaderTag.getSDP(dataSrc); + // data source should only be 3 chars long + if (dataSrc.length() > 3) { + dataSrc = dataSrc.substring(0, 3); + } + } + nameFragments.put(NameFields.DATASRC, dataSrc); + + // center lon + key = HeaderTag.CLON; + String cLon = null; + if (hdrBuilder.containsKey(key)) { + cLon = hdrBuilder.getCard(key).getValue(); + } + if (cLon == null) { + if (isGlobal) { + // set center longitude to 0.0 if value not parsed and this is a global product + cLon = "0.0"; + } else { + String errMesg = "ERROR! Could not parse CLON from fits header!"; + throw new RuntimeException(errMesg); + } + } + + // center lat + key = HeaderTag.CLAT; + String cLat = null; + if (hdrBuilder.containsKey(key)) { + cLat = hdrBuilder.getCard(key).getValue(); + } + if (cLat == null) { + if (isGlobal) { + // set center latitude to 0.0 if value not parsed and this is a global product + cLat = "0.0"; + } else { + String errMesg = "ERROR! Could not parse CLAT from fits header!"; + throw new RuntimeException(errMesg); + } + } + + String clahLon = ALTWGProductNamer.clahLon(cLat, cLon); + nameFragments.put(NameFields.CLATLON, clahLon); + + // region + String region = "l"; + if (isGlobal) { + region = "g"; + } + nameFragments.put(NameFields.REGION, region); + + // target body + key = HeaderTag.TARGET; + String tBody = "unkn"; + if (hdrBuilder.containsKey(key)) { + tBody = hdrBuilder.getCard(key).getValue(); + tBody = getBodyStFrag(tBody); + } + nameFragments.put(NameFields.TBODY, tBody); + + // pds likes having filenames all in the same case, so chose lowercase + String outFile = DartNamer.getBaseName(nameFragments); + return outFile; } - return returnFrag; - } + @Override + public String getVersion(FitsHdrBuilder hdrBuilder) { - private static void validateMap(Map nameFragments) { + String prodVer = "prodVer"; - NameFields[] reqFields = new NameFields[7]; - reqFields[0] = NameFields.REGION; - reqFields[1] = NameFields.GSD; - reqFields[2] = NameFields.DATASRC; - reqFields[3] = NameFields.DATATYPE; - reqFields[4] = NameFields.TBODY; - reqFields[5] = NameFields.CLATLON; - reqFields[6] = NameFields.VERSION; + // note: this has been changed to MAP_VER in the SIS + HeaderTag key = HeaderTag.MAP_VER; + // key = HeaderTag.PRODVERS; + if (hdrBuilder.containsKey(key)) { + prodVer = hdrBuilder.getCard(key).getValue(); + prodVer = prodVer.replaceAll("\\.", ""); + } - for (NameFields requiredField : reqFields) { - - if (!nameFragments.containsKey(requiredField)) { - String errMesg = "ERROR! Missing required field:" + requiredField.toString(); - throw new RuntimeException(errMesg); - } + return prodVer; } - } - @Override - /** Parse the filename using the DART naming convention and return the GSD value. */ - public double gsdFromFilename(String filename) { + @Override + public double gsdFromHdr(FitsHdrBuilder hdrBuilder) { - String[] splitStr = filename.split("_"); - // GSD is second element - String gsd = splitStr[1]; - gsd = gsd.replace("mm", ""); - return StringUtil.parseSafeD(gsd); - } + String gsd = "gsd"; + double gsdD = Double.NaN; + + // extract ground sample distance using GSD first + HeaderTag key = HeaderTag.GSD; + if (hdrBuilder.containsKey(key)) { + gsd = hdrBuilder.getCard(key).getValue(); + gsdD = StringUtil.parseSafeD(gsd); + if (gsdD < 0D) { + // keyword value not initialized + gsdD = Double.NaN; + System.out.println("WARNING! keyword GSD not set!"); + } + } else { + System.out.println("could not find " + key.toString() + " to parse GSD from."); + } + if (Double.isNaN(gsdD)) { + // could not parse GSD into valid number, try GSDI + key = HeaderTag.GSDI; + if (hdrBuilder.containsKey(key)) { + gsdD = StringUtil.parseSafeD(hdrBuilder.getCard(key).getValue()); + if (gsdD < 0D) { + // keyword value not initialized + gsdD = Double.NaN; + System.out.println("WARNING! keyword GSDI not set!"); + } + } else { + System.out.println("could not find " + key.toString() + " to parse GSD from."); + } + if (Double.isNaN(gsdD)) { + // still cannot parse gsd. Set to -999 + System.out.println("WARNING: No valid values of GSD or GSDI could be parsed from fits header!"); + System.out.println("Setting gsd = -999"); + gsdD = -999D; + } + } + + if (hdrBuilder.getCard(key).getComment().contains("[cm]")) { + + // mandated to use mm! change the units + gsdD = gsdD * 10.0D; + } + + return gsdD; + } + + @Override + public NameConvention getNameConvention() { + return NameConvention.DARTPRODUCT; + } + + /** + * Parse target body string to get the proper string fragment for the target body name. + * + * @param tBody + * @return + */ + private String getBodyStFrag(String tBody) { + + String returnFrag = tBody; + + if (tBody.toLowerCase().contains("didy")) { + returnFrag = "didy"; + } else { + if (tBody.toLowerCase().contains("dimo")) { + returnFrag = "dimo"; + } else { + System.out.println("Could not parse target string fragment from:" + tBody); + } + } + + return returnFrag; + } + + private static void validateMap(Map nameFragments) { + + NameFields[] reqFields = new NameFields[7]; + reqFields[0] = NameFields.REGION; + reqFields[1] = NameFields.GSD; + reqFields[2] = NameFields.DATASRC; + reqFields[3] = NameFields.DATATYPE; + reqFields[4] = NameFields.TBODY; + reqFields[5] = NameFields.CLATLON; + reqFields[6] = NameFields.VERSION; + + for (NameFields requiredField : reqFields) { + + if (!nameFragments.containsKey(requiredField)) { + String errMesg = "ERROR! Missing required field:" + requiredField.toString(); + throw new RuntimeException(errMesg); + } + } + } + + @Override + /** Parse the filename using the DART naming convention and return the GSD value. */ + public double gsdFromFilename(String filename) { + + String[] splitStr = filename.split("_"); + // GSD is second element + String gsd = splitStr[1]; + gsd = gsd.replace("mm", ""); + return StringUtil.parseSafeD(gsd); + } } diff --git a/src/main/java/terrasaur/altwg/pipeline/NameConvention.java b/src/main/java/terrasaur/altwg/pipeline/NameConvention.java index 2000861..818f719 100644 --- a/src/main/java/terrasaur/altwg/pipeline/NameConvention.java +++ b/src/main/java/terrasaur/altwg/pipeline/NameConvention.java @@ -24,25 +24,27 @@ package terrasaur.altwg.pipeline; /** * Enum to store the different types of naming conventions. - * + * * @author espirrc1 * */ public enum NameConvention { + ALTPRODUCT, + ALTNFTMLN, + DARTPRODUCT, + NOMATCH, + NONEUSED; - ALTPRODUCT, ALTNFTMLN, DARTPRODUCT, NOMATCH, NONEUSED; - - public static NameConvention parseNameConvention(String name) { - for (NameConvention nameConvention : values()) { - if (nameConvention.toString().toLowerCase().equals(name.toLowerCase())) { - System.out.println("parsed naming convention:" + nameConvention.toString()); + public static NameConvention parseNameConvention(String name) { + for (NameConvention nameConvention : values()) { + if (nameConvention.toString().toLowerCase().equals(name.toLowerCase())) { + System.out.println("parsed naming convention:" + nameConvention.toString()); + return nameConvention; + } + } + NameConvention nameConvention = NameConvention.NOMATCH; + System.out.println("NameConvention.parseNameConvention()" + " could not parse naming convention:" + name + + ". Returning:" + nameConvention.toString()); return nameConvention; - } } - NameConvention nameConvention = NameConvention.NOMATCH; - System.out - .println("NameConvention.parseNameConvention()" + " could not parse naming convention:" - + name + ". Returning:" + nameConvention.toString()); - return nameConvention; - } } diff --git a/src/main/java/terrasaur/altwg/pipeline/NameFields.java b/src/main/java/terrasaur/altwg/pipeline/NameFields.java index beb0d84..be02956 100644 --- a/src/main/java/terrasaur/altwg/pipeline/NameFields.java +++ b/src/main/java/terrasaur/altwg/pipeline/NameFields.java @@ -25,11 +25,16 @@ package terrasaur.altwg.pipeline; /** * Enum to describe the different parts of the product name. Used by concrete classes implementing * ProductNamer. - * + * * @author espirrc1 * */ public enum NameFields { - - GSD, DATATYPE, VERSION, DATASRC, CLATLON, REGION, TBODY; + GSD, + DATATYPE, + VERSION, + DATASRC, + CLATLON, + REGION, + TBODY; } diff --git a/src/main/java/terrasaur/altwg/pipeline/NamingFactory.java b/src/main/java/terrasaur/altwg/pipeline/NamingFactory.java index 7ac77de..fa87bd1 100644 --- a/src/main/java/terrasaur/altwg/pipeline/NamingFactory.java +++ b/src/main/java/terrasaur/altwg/pipeline/NamingFactory.java @@ -35,99 +35,97 @@ import terrasaur.fits.FitsHdr.FitsHdrBuilder; */ public class NamingFactory { - public static ProductNamer getNamingConvention(NameConvention namingConvention) { + public static ProductNamer getNamingConvention(NameConvention namingConvention) { - switch (namingConvention) { - case ALTPRODUCT: - return new ALTWGProductNamer(); + switch (namingConvention) { + case ALTPRODUCT: + return new ALTWGProductNamer(); - case ALTNFTMLN: - return new AltwgMLNNamer(); + case ALTNFTMLN: + return new AltwgMLNNamer(); - case DARTPRODUCT: - return new DartNamer(); + case DARTPRODUCT: + return new DartNamer(); - default: - System.err.println( - "ERROR! Naming convention:" + namingConvention.toString() + " not supported!"); - throw new RuntimeException(); - } - } - - /** - * Parse for keyword in pipeline config file that specifies what naming convention to use. - * - * @param pipeConfig - * @return - */ - public static ProductNamer parseNamingConvention( - Map pipeConfig, boolean verbose) { - - ProductNamer productNamer = null; - - if ((pipeConfig.containsKey(AltPipelnEnum.NAMINGCONVENTION))) { - String value = pipeConfig.get(AltPipelnEnum.NAMINGCONVENTION); - productNamer = parseNamingConvention(value); - } else { - String errMesg = "ERROR! Naming convention should have been defined in pipeConfig!"; - throw new RuntimeException(errMesg); + default: + System.err.println("ERROR! Naming convention:" + namingConvention.toString() + " not supported!"); + throw new RuntimeException(); + } } - return productNamer; - } + /** + * Parse for keyword in pipeline config file that specifies what naming convention to use. + * + * @param pipeConfig + * @return + */ + public static ProductNamer parseNamingConvention(Map pipeConfig, boolean verbose) { - /** - * Parse string to determine the naming convention to use. Naming convention supplied by - * ProductNamer interface. - * - * @param value - * @return - */ - public static ProductNamer parseNamingConvention(String value) { + ProductNamer productNamer = null; - if (value.length() < 1) { - String errMesg = "ERROR! Cannot pass empty string to NamingFactory.parseNamingConvention!"; - throw new RuntimeException(errMesg); - } - NameConvention nameConvention = NameConvention.parseNameConvention(value); - return NamingFactory.getNamingConvention(nameConvention); - } + if ((pipeConfig.containsKey(AltPipelnEnum.NAMINGCONVENTION))) { + String value = pipeConfig.get(AltPipelnEnum.NAMINGCONVENTION); + productNamer = parseNamingConvention(value); + } else { + String errMesg = "ERROR! Naming convention should have been defined in pipeConfig!"; + throw new RuntimeException(errMesg); + } - /** - * Given the naming convention, hdrBuilder, productType, and original output file, return the - * renamed output file and cross reference file. Output is returned as a File[] array where - * array[0] is the output basename, array[1] is the cross-reference file. If no naming convention - * is specified (NONEUSED) then array[0] is the same as outfile, array[1] is null. - * - * @param namingConvention - * @param hdrBuilder - * @param productType - * @param isGlobal - * @param outfile - proposed output filename. If Naming convention results in a renamed OBJ then - * this is not used. If no naming convention specified then outputFiles[0] = outfile. - * @return - */ - public static File[] getBaseNameAndCrossRef( - NameConvention namingConvention, - FitsHdrBuilder hdrBuilder, - AltwgDataType productType, - boolean isGlobal, - String outfile) { - - File[] outputFiles = new File[2]; - - // default to no renaming. - File crossrefFile = null; - String basename = outfile; - - if (namingConvention != NameConvention.NONEUSED) { - ProductNamer productNamer = NamingFactory.getNamingConvention(namingConvention); - basename = productNamer.productbaseName(hdrBuilder, productType, isGlobal); - crossrefFile = new File(outfile + ".crf"); + return productNamer; } - outputFiles[0] = new File(basename); - outputFiles[1] = crossrefFile; - return outputFiles; - } + /** + * Parse string to determine the naming convention to use. Naming convention supplied by + * ProductNamer interface. + * + * @param value + * @return + */ + public static ProductNamer parseNamingConvention(String value) { + + if (value.length() < 1) { + String errMesg = "ERROR! Cannot pass empty string to NamingFactory.parseNamingConvention!"; + throw new RuntimeException(errMesg); + } + NameConvention nameConvention = NameConvention.parseNameConvention(value); + return NamingFactory.getNamingConvention(nameConvention); + } + + /** + * Given the naming convention, hdrBuilder, productType, and original output file, return the + * renamed output file and cross reference file. Output is returned as a File[] array where + * array[0] is the output basename, array[1] is the cross-reference file. If no naming convention + * is specified (NONEUSED) then array[0] is the same as outfile, array[1] is null. + * + * @param namingConvention + * @param hdrBuilder + * @param productType + * @param isGlobal + * @param outfile - proposed output filename. If Naming convention results in a renamed OBJ then + * this is not used. If no naming convention specified then outputFiles[0] = outfile. + * @return + */ + public static File[] getBaseNameAndCrossRef( + NameConvention namingConvention, + FitsHdrBuilder hdrBuilder, + AltwgDataType productType, + boolean isGlobal, + String outfile) { + + File[] outputFiles = new File[2]; + + // default to no renaming. + File crossrefFile = null; + String basename = outfile; + + if (namingConvention != NameConvention.NONEUSED) { + ProductNamer productNamer = NamingFactory.getNamingConvention(namingConvention); + basename = productNamer.productbaseName(hdrBuilder, productType, isGlobal); + crossrefFile = new File(outfile + ".crf"); + } + + outputFiles[0] = new File(basename); + outputFiles[1] = crossrefFile; + return outputFiles; + } } diff --git a/src/main/java/terrasaur/altwg/pipeline/ProductNamer.java b/src/main/java/terrasaur/altwg/pipeline/ProductNamer.java index 652cc30..cc76337 100644 --- a/src/main/java/terrasaur/altwg/pipeline/ProductNamer.java +++ b/src/main/java/terrasaur/altwg/pipeline/ProductNamer.java @@ -27,17 +27,15 @@ import terrasaur.fits.FitsHdr.FitsHdrBuilder; public interface ProductNamer { - public String getNameFrag(String productName, int fieldNum); + public String getNameFrag(String productName, int fieldNum); - public String productbaseName(FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, - boolean isGlobal); + public String productbaseName(FitsHdrBuilder hdrBuilder, AltwgDataType altwgProduct, boolean isGlobal); - public String getVersion(FitsHdrBuilder hdrBuilder); + public String getVersion(FitsHdrBuilder hdrBuilder); - public double gsdFromHdr(FitsHdrBuilder hdrBuilder); + public double gsdFromHdr(FitsHdrBuilder hdrBuilder); - public NameConvention getNameConvention(); - - public double gsdFromFilename(String filename); + public NameConvention getNameConvention(); + public double gsdFromFilename(String filename); } diff --git a/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java b/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java index 502a689..1a7f411 100644 --- a/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java +++ b/src/main/java/terrasaur/apps/AdjustShapeModelToOtherShapeModel.java @@ -54,51 +54,45 @@ import vtk.vtksbCellLocator; */ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Adjust vertices of one shape model to lie on the surface of another."; - } + @Override + public String shortDescription() { + return "Adjust vertices of one shape model to lie on the surface of another."; + } - @Override - public String fullDescription(Options options) { + @Override + public String fullDescription(Options options) { - String header = - """ + String header = + """ \n This program takes 2 shape models in OBJ format and tries to adjust to vertices of the first shape model so they lie on the surface of the second shape model. It does this by shooting a ray starting from the origin in the direction of each point of the first model into the second model and then changes the point of the first model to the intersection point."""; - return TerrasaurTool.super.fullDescription(options, header, ""); - } + return TerrasaurTool.super.fullDescription(options, header, ""); + } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("from") - .hasArg() - .desc( - "path to first shape model in OBJ format which will get shifted to the second shape model") - .build()); - options.addOption( - Option.builder("to") - .hasArg() - .desc( - "path to second shape model in OBJ format which the first shape model will try to match to") - .build()); - options.addOption( - Option.builder("output") - .hasArg() - .desc( - "path to adjusted shape model in OBJ format generated by this program by shifting first to second") - .build()); - options.addOption( - Option.builder("filelist") - .desc( - """ + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("from") + .hasArg() + .desc("path to first shape model in OBJ format which will get shifted to the second shape model") + .build()); + options.addOption(Option.builder("to") + .hasArg() + .desc("path to second shape model in OBJ format which the first shape model will try to match to") + .build()); + options.addOption(Option.builder("output") + .hasArg() + .desc( + "path to adjusted shape model in OBJ format generated by this program by shifting first to second") + .build()); + options.addOption(Option.builder("filelist") + .desc( + """ If specified then the second required argument to this program, "to" is a file containing a list of OBJ files to match to. In this situation the ray is shot into each of the shape models in this @@ -107,201 +101,177 @@ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool { list may be only a piece of the the complete shape model (e.g. a mapola). However, the global shape model formed when all these pieces are combined together, may not have any holes or gaps.""") - .build()); - options.addOption( - Option.builder("fit-plane-radius") - .hasArg() - .desc( - """ + .build()); + options.addOption(Option.builder("fit-plane-radius") + .hasArg() + .desc( + """ If present, find a local normal at each point in the first shape model by fitting a plane to all points within the specified radius. Use this normal to adjust the point to the second shape model rather than the radial vector.""") - .build()); - options.addOption( - Option.builder("local") - .desc( - """ + .build()); + options.addOption(Option.builder("local") + .desc( + """ Use when adjusting a local OBJ file to another. The best fit plane to the first shape model is used to adjust the vertices rather than the radial vector for each point. """) - .build()); - return options; - } - - static Vector3D computeMeanPoint(List points) { - Vector3D meanPoint = new Vector3D(0., 0., 0.); - for (Vector3D point : points) meanPoint = meanPoint.add(point); - meanPoint = meanPoint.scalarMultiply(1. / points.size()); - return meanPoint; - } - - public static void adjustShapeModelToOtherShapeModel( - vtkPolyData frompolydata, - ArrayList topolydata, - double planeRadius, - boolean localModel) - throws Exception { - vtkPoints points = frompolydata.GetPoints(); - long numberPoints = frompolydata.GetNumberOfPoints(); - - boolean fitPlane = (planeRadius > 0); - SmallBodyModel sbModel = new SmallBodyModel(frompolydata); - double diagonalLength = new BoundingBox(frompolydata.GetBounds()).getDiagonalLength(); - - ArrayList cellLocators = new ArrayList<>(); - for (vtkPolyData polydata : topolydata) { - vtksbCellLocator cellLocator = new vtksbCellLocator(); - cellLocator.SetDataSet(polydata); - cellLocator.CacheCellBoundsOn(); - cellLocator.AutomaticOn(); - cellLocator.BuildLocator(); - cellLocators.add(cellLocator); + .build()); + return options; } - vtkGenericCell cell = new vtkGenericCell(); - double tol = 1e-6; - double[] t = new double[1]; - double[] pcoords = new double[3]; - int[] subId = new int[1]; - long[] cell_id = new long[1]; - - double[] localNormal = null; - if (localModel) { - // fit a plane to the local model and check that the normal points outward - Plane localPlane = PolyDataUtil.fitPlaneToPolyData(frompolydata); - Vector3 localNormalVector = localPlane.getNormal(); - if (localNormalVector.dot(localPlane.getPoint()) < 0) - localNormalVector = localNormalVector.negate(); - localNormal = localNormalVector.toArray(); + static Vector3D computeMeanPoint(List points) { + Vector3D meanPoint = new Vector3D(0., 0., 0.); + for (Vector3D point : points) meanPoint = meanPoint.add(point); + meanPoint = meanPoint.scalarMultiply(1. / points.size()); + return meanPoint; } - double[] p = new double[3]; - Vector3D origin = new Vector3D(0., 0., 0.); - for (int i = 0; i < numberPoints; ++i) { - points.GetPoint(i, p); - Vector3D thisPoint = new Vector3D(p); + public static void adjustShapeModelToOtherShapeModel( + vtkPolyData frompolydata, ArrayList topolydata, double planeRadius, boolean localModel) + throws Exception { + vtkPoints points = frompolydata.GetPoints(); + long numberPoints = frompolydata.GetNumberOfPoints(); - Vector3D lookDir; + boolean fitPlane = (planeRadius > 0); + SmallBodyModel sbModel = new SmallBodyModel(frompolydata); + double diagonalLength = new BoundingBox(frompolydata.GetBounds()).getDiagonalLength(); - if (fitPlane) { - // fit a plane to the local area - System.arraycopy(p, 0, origin.toArray(), 0, 3); - lookDir = new Vector3D(sbModel.getNormalAtPoint(p, planeRadius)).normalize(); - } else if (localModel) { - System.arraycopy(p, 0, origin.toArray(), 0, 3); - lookDir = new Vector3D(localNormal).normalize(); - } else { - // use radial vector - lookDir = new Vector3D(p).normalize(); - } - - Vector3D lookPt = lookDir.scalarMultiply(diagonalLength); - lookPt = lookPt.add(thisPoint); - - List intersections = new ArrayList<>(); - for (vtksbCellLocator cellLocator : cellLocators) { - double[] intersectPoint = new double[3]; - - // 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(), - tol, - t, - intersectPoint, - pcoords, - subId, - cell_id, - cell); - Vector3D intersectVector = new Vector3D(intersectPoint); - - NavigableMap pointsMap = new TreeMap<>(); - if (intersectVector.getNorm() > 0) { - pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector); + ArrayList cellLocators = new ArrayList<>(); + for (vtkPolyData polydata : topolydata) { + vtksbCellLocator cellLocator = new vtksbCellLocator(); + cellLocator.SetDataSet(polydata); + cellLocator.CacheCellBoundsOn(); + cellLocator.AutomaticOn(); + cellLocator.BuildLocator(); + cellLocators.add(cellLocator); } - // 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); + vtkGenericCell cell = new vtkGenericCell(); + double tol = 1e-6; + double[] t = new double[1]; + double[] pcoords = new double[3]; + int[] subId = new int[1]; + long[] cell_id = new long[1]; - intersectVector = new Vector3D(intersectPoint); - if (intersectVector.getNorm() > 0) { - pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector); + double[] localNormal = null; + if (localModel) { + // fit a plane to the local model and check that the normal points outward + Plane localPlane = PolyDataUtil.fitPlaneToPolyData(frompolydata); + Vector3 localNormalVector = localPlane.getNormal(); + if (localNormalVector.dot(localPlane.getPoint()) < 0) localNormalVector = localNormalVector.negate(); + localNormal = localNormalVector.toArray(); } - if (!pointsMap.isEmpty()) intersections.add(pointsMap.get(pointsMap.firstKey())); - } + double[] p = new double[3]; + Vector3D origin = new Vector3D(0., 0., 0.); + for (int i = 0; i < numberPoints; ++i) { + points.GetPoint(i, p); + Vector3D thisPoint = new Vector3D(p); - if (intersections.isEmpty()) throw new Exception("Error: no intersections at all"); + Vector3D lookDir; - Vector3D meanIntersectionPoint = computeMeanPoint(intersections); - points.SetPoint(i, meanIntersectionPoint.toArray()); - } - } + if (fitPlane) { + // fit a plane to the local area + System.arraycopy(p, 0, origin.toArray(), 0, 3); + lookDir = new Vector3D(sbModel.getNormalAtPoint(p, planeRadius)).normalize(); + } else if (localModel) { + System.arraycopy(p, 0, origin.toArray(), 0, 3); + lookDir = new Vector3D(localNormal).normalize(); + } else { + // use radial vector + lookDir = new Vector3D(p).normalize(); + } - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new AdjustShapeModelToOtherShapeModel(); + Vector3D lookPt = lookDir.scalarMultiply(diagonalLength); + lookPt = lookPt.add(thisPoint); - Options options = defineOptions(); + List intersections = new ArrayList<>(); + for (vtksbCellLocator cellLocator : cellLocators) { + double[] intersectPoint = new double[3]; - CommandLine cl = defaultOBJ.parseArgs(args, options); + // 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(), tol, t, intersectPoint, pcoords, subId, cell_id, cell); + Vector3D intersectVector = new Vector3D(intersectPoint); - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + NavigableMap pointsMap = new TreeMap<>(); + if (intersectVector.getNorm() > 0) { + pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector); + } - boolean loadListFromFile = cl.hasOption("filelist"); - double planeRadius = Double.parseDouble(cl.getOptionValue("fit-plane-radius", "-1")); - boolean localModel = cl.hasOption("local"); + // 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); - NativeLibraryLoader.loadVtkLibraries(); + intersectVector = new Vector3D(intersectPoint); + if (intersectVector.getNorm() > 0) { + pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector); + } - String fromfile = cl.getOptionValue("from"); - String tofile = cl.getOptionValue("to"); - String outfile = cl.getOptionValue("output"); + if (!pointsMap.isEmpty()) intersections.add(pointsMap.get(pointsMap.firstKey())); + } - Log4j2Configurator.getInstance(); - logger.info("loading : {}", fromfile); - vtkPolyData frompolydata = PolyDataUtil.loadShapeModelAndComputeNormals(fromfile); + if (intersections.isEmpty()) throw new Exception("Error: no intersections at all"); - ArrayList topolydata = new ArrayList<>(); - if (loadListFromFile) { - List lines = FileUtils.readLines(new File(tofile), Charset.defaultCharset()); - for (String file : lines) { - - // checking length prevents trying to load an empty line, such as the - // last line of the file. - if (file.length() > 1) { - logger.info("loading : {}", file); - topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(file)); + Vector3D meanIntersectionPoint = computeMeanPoint(intersections); + points.SetPoint(i, meanIntersectionPoint.toArray()); } - } - } else { - logger.info("loading : {}", tofile); - topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(tofile)); } - adjustShapeModelToOtherShapeModel(frompolydata, topolydata, planeRadius, localModel); + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new AdjustShapeModelToOtherShapeModel(); - PolyDataUtil.saveShapeModelAsOBJ(frompolydata, outfile); + Options options = defineOptions(); - logger.info("wrote {}", outfile); - } + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + boolean loadListFromFile = cl.hasOption("filelist"); + double planeRadius = Double.parseDouble(cl.getOptionValue("fit-plane-radius", "-1")); + boolean localModel = cl.hasOption("local"); + + NativeLibraryLoader.loadVtkLibraries(); + + String fromfile = cl.getOptionValue("from"); + String tofile = cl.getOptionValue("to"); + String outfile = cl.getOptionValue("output"); + + Log4j2Configurator.getInstance(); + logger.info("loading : {}", fromfile); + vtkPolyData frompolydata = PolyDataUtil.loadShapeModelAndComputeNormals(fromfile); + + ArrayList topolydata = new ArrayList<>(); + if (loadListFromFile) { + List lines = FileUtils.readLines(new File(tofile), Charset.defaultCharset()); + for (String file : lines) { + + // checking length prevents trying to load an empty line, such as the + // last line of the file. + if (file.length() > 1) { + logger.info("loading : {}", file); + topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(file)); + } + } + } else { + logger.info("loading : {}", tofile); + topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(tofile)); + } + + adjustShapeModelToOtherShapeModel(frompolydata, topolydata, planeRadius, localModel); + + PolyDataUtil.saveShapeModelAsOBJ(frompolydata, outfile); + + logger.info("wrote {}", outfile); + } } diff --git a/src/main/java/terrasaur/apps/AppendOBJ.java b/src/main/java/terrasaur/apps/AppendOBJ.java index 9a016c8..36e6c72 100644 --- a/src/main/java/terrasaur/apps/AppendOBJ.java +++ b/src/main/java/terrasaur/apps/AppendOBJ.java @@ -44,121 +44,117 @@ import vtk.vtkPolyData; */ public class AppendOBJ implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Combine multiple shape files (OBJ or VTK format) into one."; - } - - @Override - public String fullDescription(Options options) { - String header = "This program combines input shape models into a single shape model."; - return TerrasaurTool.super.fullDescription(options, header, ""); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - - options.addOption( - Option.builder("boundary") - .desc("Only save out boundary. This option implies -vtk.") - .build()); - options.addOption( - Option.builder("decimate") - .hasArg() - .desc( - "Reduce the number of facets in the output shape model. The argument should be between 0 and 1. " - + "For example, if a model has 100 facets and is 0.90, " - + "there will be approximately 10 facets after the decimation.") - .build()); - options.addOption( - Option.builder("input") - .required() - .hasArgs() - .desc( - "input file(s) to read. Format is derived from the allowed extension: " - + "icq, llr, obj, pds, plt, ply, stl, or vtk. Multiple files can be specified " - + "with a single -input option, separated by whitespace. Alternatively, you may " - + "specify -input multiple times.") - .build()); - options.addOption( - Option.builder("output").required().hasArg().desc("output file to write.").build()); - options.addOption( - Option.builder("vtk").desc("Save output file in VTK format rather than OBJ.").build()); - return options; - } - - public static void main(String[] args) throws Exception { - - TerrasaurTool defaultOBJ = new AppendOBJ(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - boolean boundaryOnly = cl.hasOption("boundary"); - boolean vtkFormat = boundaryOnly || cl.hasOption("vtk"); - boolean decimate = cl.hasOption("decimate"); - double decimationPercentage = - decimate ? Double.parseDouble(cl.getOptionValue("decimate")) : 1.0; - - NativeLibraryLoader.loadVtkLibraries(); - - String outfile = cl.getOptionValue("output"); - String[] infiles = cl.getOptionValues("input"); - - vtkAppendPolyData append = new vtkAppendPolyData(); - append.UserManagedInputsOn(); - append.SetNumberOfInputs(infiles.length); - - for (int i = 0; i < infiles.length; ++i) { - logger.info("loading {} {} / {}", infiles[i], i + 1, infiles.length); - - vtkPolyData polydata = PolyDataUtil.loadShapeModel(infiles[i]); - - if (polydata == null) { - logger.warn("Cannot load {}", infiles[i]); - } else { - if (boundaryOnly) { - vtkPolyData boundary = PolyDataUtil.getBoundary(polydata); - boundary.GetCellData().SetScalars(null); - polydata.DeepCopy(boundary); - } - - append.SetInputDataByNumber(i, polydata); - } - System.gc(); - vtkObjectBase.JAVA_OBJECT_MANAGER.gc(false); + @Override + public String shortDescription() { + return "Combine multiple shape files (OBJ or VTK format) into one."; } - append.Update(); + @Override + public String fullDescription(Options options) { + String header = "This program combines input shape models into a single shape model."; + return TerrasaurTool.super.fullDescription(options, header, ""); + } - vtkPolyData outputShape = append.GetOutput(); - if (decimate) PolyDataUtil.decimatePolyData(outputShape, decimationPercentage); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); - if (vtkFormat) PolyDataUtil.saveShapeModelAsVTK(outputShape, outfile); - else PolyDataUtil.saveShapeModelAsOBJ(append.GetOutput(), outfile); + options.addOption(Option.builder("boundary") + .desc("Only save out boundary. This option implies -vtk.") + .build()); + options.addOption(Option.builder("decimate") + .hasArg() + .desc( + "Reduce the number of facets in the output shape model. The argument should be between 0 and 1. " + + "For example, if a model has 100 facets and is 0.90, " + + "there will be approximately 10 facets after the decimation.") + .build()); + options.addOption(Option.builder("input") + .required() + .hasArgs() + .desc("input file(s) to read. Format is derived from the allowed extension: " + + "icq, llr, obj, pds, plt, ply, stl, or vtk. Multiple files can be specified " + + "with a single -input option, separated by whitespace. Alternatively, you may " + + "specify -input multiple times.") + .build()); + options.addOption(Option.builder("output") + .required() + .hasArg() + .desc("output file to write.") + .build()); + options.addOption(Option.builder("vtk") + .desc("Save output file in VTK format rather than OBJ.") + .build()); + return options; + } - logger.info("Wrote " + outfile); - } + public static void main(String[] args) throws Exception { + + TerrasaurTool defaultOBJ = new AppendOBJ(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + boolean boundaryOnly = cl.hasOption("boundary"); + boolean vtkFormat = boundaryOnly || cl.hasOption("vtk"); + boolean decimate = cl.hasOption("decimate"); + double decimationPercentage = decimate ? Double.parseDouble(cl.getOptionValue("decimate")) : 1.0; + + NativeLibraryLoader.loadVtkLibraries(); + + String outfile = cl.getOptionValue("output"); + String[] infiles = cl.getOptionValues("input"); + + vtkAppendPolyData append = new vtkAppendPolyData(); + append.UserManagedInputsOn(); + append.SetNumberOfInputs(infiles.length); + + for (int i = 0; i < infiles.length; ++i) { + logger.info("loading {} {} / {}", infiles[i], i + 1, infiles.length); + + vtkPolyData polydata = PolyDataUtil.loadShapeModel(infiles[i]); + + if (polydata == null) { + logger.warn("Cannot load {}", infiles[i]); + } else { + if (boundaryOnly) { + vtkPolyData boundary = PolyDataUtil.getBoundary(polydata); + boundary.GetCellData().SetScalars(null); + polydata.DeepCopy(boundary); + } + + append.SetInputDataByNumber(i, polydata); + } + System.gc(); + vtkObjectBase.JAVA_OBJECT_MANAGER.gc(false); + } + + append.Update(); + + vtkPolyData outputShape = append.GetOutput(); + if (decimate) PolyDataUtil.decimatePolyData(outputShape, decimationPercentage); + + if (vtkFormat) PolyDataUtil.saveShapeModelAsVTK(outputShape, outfile); + else PolyDataUtil.saveShapeModelAsOBJ(append.GetOutput(), outfile); + + logger.info("Wrote " + outfile); + } } diff --git a/src/main/java/terrasaur/apps/BatchSubmit.java b/src/main/java/terrasaur/apps/BatchSubmit.java index b7a8d64..2546882 100644 --- a/src/main/java/terrasaur/apps/BatchSubmit.java +++ b/src/main/java/terrasaur/apps/BatchSubmit.java @@ -39,72 +39,65 @@ import terrasaur.utils.batch.GridType; public class BatchSubmit implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - - @Override - public String shortDescription() { - return "Run a command on a cluster."; - } - - @Override - public String fullDescription(Options options) { - - String footer = "\nRun a command on a cluster.\n"; - - return TerrasaurTool.super.fullDescription(options, "", footer); - - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("command") - .required() - .hasArgs() - .desc("Required. Command(s) to run.") - .build()); - - StringBuilder sb = new StringBuilder(); - for (GridType type : GridType.values()) sb.append(String.format("%s ", type.name())); - options.addOption( - Option.builder("gridType") - .hasArg() - .desc("Grid type. Valid values are " + sb + ". Default is LOCAL.") - .build()); - - options.addOption( - Option.builder("workingDir") - .hasArg() - .desc("Working directory to run command. Default is current directory.") - .build()); - return options; + @Override + public String shortDescription() { + return "Run a command on a cluster."; } - public static void main(String[] args) { + @Override + public String fullDescription(Options options) { - TerrasaurTool defaultOBJ = new BatchSubmit(); + String footer = "\nRun a command on a cluster.\n"; - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - List cmdList = Arrays.asList(cl.getOptionValues("command")); - BatchType batchType = BatchType.GRID_ENGINE; - GridType gridType = - cl.hasOption("gridType") ? GridType.valueOf(cl.getOptionValue("gridType")) : GridType.LOCAL; - - BatchSubmitI submitter = BatchSubmitFactory.getBatchSubmit(cmdList, batchType, gridType); - String workingDir = ""; - try { - submitter.runBatchSubmitinDir(workingDir); - } catch (InterruptedException | IOException e) { - logger.error(e.getLocalizedMessage(), e); + return TerrasaurTool.super.fullDescription(options, "", footer); } - } + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("command") + .required() + .hasArgs() + .desc("Required. Command(s) to run.") + .build()); + + StringBuilder sb = new StringBuilder(); + for (GridType type : GridType.values()) sb.append(String.format("%s ", type.name())); + options.addOption(Option.builder("gridType") + .hasArg() + .desc("Grid type. Valid values are " + sb + ". Default is LOCAL.") + .build()); + + options.addOption(Option.builder("workingDir") + .hasArg() + .desc("Working directory to run command. Default is current directory.") + .build()); + return options; + } + + public static void main(String[] args) { + + TerrasaurTool defaultOBJ = new BatchSubmit(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + List cmdList = Arrays.asList(cl.getOptionValues("command")); + BatchType batchType = BatchType.GRID_ENGINE; + GridType gridType = cl.hasOption("gridType") ? GridType.valueOf(cl.getOptionValue("gridType")) : GridType.LOCAL; + + BatchSubmitI submitter = BatchSubmitFactory.getBatchSubmit(cmdList, batchType, gridType); + String workingDir = ""; + try { + submitter.runBatchSubmitinDir(workingDir); + } catch (InterruptedException | IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } } diff --git a/src/main/java/terrasaur/apps/CKFromSumFile.java b/src/main/java/terrasaur/apps/CKFromSumFile.java index fc41735..ae6ee74 100644 --- a/src/main/java/terrasaur/apps/CKFromSumFile.java +++ b/src/main/java/terrasaur/apps/CKFromSumFile.java @@ -64,35 +64,46 @@ import terrasaur.utils.math.MathConversions; public class CKFromSumFile implements TerrasaurTool { - private final static Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Create a CK from a list of sumfiles."; - } + @Override + public String shortDescription() { + return "Create a CK from a list of sumfiles."; + } - @Override - public String fullDescription(Options options) { - String footer = "Create a CK from a list of sumfiles."; - return TerrasaurTool.super.fullDescription(options, "", footer); - } + @Override + public String fullDescription(Options options) { + String footer = "Create a CK from a list of sumfiles."; + return TerrasaurTool.super.fullDescription(options, "", footer); + } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("config").required().hasArg() - .desc("Required. Name of configuration file.").build()); - options.addOption(Option.builder("dumpConfig").hasArg() - .desc("Write out an example configuration to the named file.").build()); - options.addOption(Option.builder("logFile").hasArg() - .desc("If present, save screen output to log file.").build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) - sb.append(String.format("%s ", l.name())); - options.addOption(Option.builder("logLevel").hasArg() - .desc("If present, print messages above selected priority. Valid values are " - + sb.toString().trim() + ". Default is INFO.") - .build()); - options.addOption(Option.builder("sumFile").hasArg().required().desc(""" + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("config") + .required() + .hasArg() + .desc("Required. Name of configuration file.") + .build()); + options.addOption(Option.builder("dumpConfig") + .hasArg() + .desc("Write out an example configuration to the named file.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + ". Default is INFO.") + .build()); + options.addOption(Option.builder("sumFile") + .hasArg() + .required() + .desc( + """ Required. File listing sumfiles to read. This is a text file, one per line. Lines starting with # are ignored. @@ -106,212 +117,212 @@ public class CKFromSumFile implements TerrasaurTool { D717506131G0.SUM # This is a comment D717506132G0.SUM - """).build()); - return options; - } - - private final CKFromSumFileConfig config; - private final NavigableMap sumFiles; - - private CKFromSumFile(){config=null;sumFiles=null;} - - public CKFromSumFile(CKFromSumFileConfig config, NavigableMap sumFiles) { - this.config = config; - this.sumFiles = sumFiles; - } - - public String writeMSOPCKFiles(String basename, List comments) throws SpiceException { - - ReferenceFrame instrFrame = new ReferenceFrame(config.instrumentFrameName()); - ReferenceFrame scFrame = new ReferenceFrame(config.spacecraftFrame()); - ReferenceFrame j2000 = new ReferenceFrame("J2000"); - ReferenceFrame bodyFixed = new ReferenceFrame(config.bodyFrame()); - - ReferenceFrame ref = config.J2000() ? j2000 : bodyFixed; - - logger.debug("Body fixed frame: {}", bodyFixed.getName()); - logger.debug("Instrument frame: {}", instrFrame.getName()); - logger.debug("Spacecraft frame: {}", scFrame.getName()); - logger.debug(" Reference frame: {}", ref.getName()); - - File commentFile = new File(basename + "-comments.txt"); - if (commentFile.exists()) - if (!commentFile.delete()) - logger.error("{} exists but cannot be deleted!", commentFile.getPath()); - - String setupFile = basename + ".setup"; - String inputFile = basename + ".inp"; - - try (PrintWriter pw = new PrintWriter(commentFile)) { - StringBuilder sb = new StringBuilder(); - - DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss z"); - ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); - - sb.append("This CK was created on ").append(dtf.format(now)).append(" from the following sumFiles:\n"); - for (SumFile sumFile : sumFiles.keySet()) { - sb.append(String.format("\t%s %s\n", sumFile.utcString(), sumFiles.get(sumFile))); - } - sb.append("\n"); - sb.append("providing the orientation of ").append(scFrame.getName()).append(" with respect to ").append(config.J2000() ? "J2000" : bodyFixed.getName()).append(". "); - double first = new TDBTime(sumFiles.firstKey().utcString()).getTDBSeconds(); - double last = new TDBTime(sumFiles.lastKey().utcString()).getTDBSeconds() + config.extend(); - sb.append("The coverage period is ").append(new TDBTime(first).toUTCString("ISOC", 3)).append(" to ").append(new TDBTime(last).toUTCString("ISOC", 3)).append(" UTC."); - - String allComments = sb.toString(); - for (String comment : allComments.split("\\r?\\n")) - pw.println(WordUtils.wrap(comment, 80)); - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); + """) + .build()); + return options; } - Map map = new TreeMap<>(); - map.put("LSK_FILE_NAME", "'" + config.lsk() + "'"); - map.put("SCLK_FILE_NAME", "'" + config.sclk() + "'"); - map.put("CK_TYPE", "3"); - map.put("COMMENTS_FILE_NAME", String.format("'%s'", commentFile.getPath())); - map.put("INSTRUMENT_ID", String.format("%d", scFrame.getIDCode())); - map.put("REFERENCE_FRAME_NAME", - String.format("'%s'", config.J2000() ? "J2000" : bodyFixed.getName())); - if (!config.fk().isEmpty()) - map.put("FRAMES_FILE_NAME", "'" + config.fk() + "'"); - map.put("ANGULAR_RATE_PRESENT", "'MAKE UP/NO AVERAGING'"); - map.put("INPUT_TIME_TYPE", "'UTC'"); - map.put("INPUT_DATA_TYPE", "'SPICE QUATERNIONS'"); - map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); + private final CKFromSumFileConfig config; + private final NavigableMap sumFiles; - try (PrintWriter pw = new PrintWriter(setupFile)) { - pw.println("\\begindata"); - for (String key : map.keySet()) { - pw.printf("%s = %s\n", key, map.get(key)); - } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); + private CKFromSumFile() { + config = null; + sumFiles = null; } - NavigableMap attitudeMap = new TreeMap<>(); - for (SumFile s : sumFiles.keySet()) { - TDBTime t = new TDBTime(s.utcString()); - - Vector3[] rows = new Vector3[3]; - rows[0] = MathConversions.toVector3(s.cx()); - rows[1] = MathConversions.toVector3(s.cy()); - rows[2] = MathConversions.toVector3(s.cz()); - - Vector3 row0 = rows[Math.abs(config.flipX()) - 1]; - Vector3 row1 = rows[Math.abs(config.flipY()) - 1]; - Vector3 row2 = rows[Math.abs(config.flipZ()) - 1]; - - if (config.flipX() < 0) - row0 = row0.negate(); - if (config.flipY() < 0) - row1 = row1.negate(); - if (config.flipZ() < 0) - row2 = row2.negate(); - - Matrix33 refToInstr = new Matrix33(row0, row1, row2); - - if (config.J2000()) { - Matrix33 j2000ToBodyFixed = j2000.getPositionTransformation(bodyFixed, t); - refToInstr = refToInstr.mxm(j2000ToBodyFixed); - } - - Matrix33 instrToSc = instrFrame.getPositionTransformation(scFrame, t); - Matrix33 refToSc = instrToSc.mxm(refToInstr); - - SpiceQuaternion q = new SpiceQuaternion(refToSc); - attitudeMap.put(t.getTDBSeconds(), q); + public CKFromSumFile(CKFromSumFileConfig config, NavigableMap sumFiles) { + this.config = config; + this.sumFiles = sumFiles; } - if (config.extend() > 0) { - var lastEntry = attitudeMap.lastEntry(); - attitudeMap.put(lastEntry.getKey() + config.extend(), lastEntry.getValue()); + public String writeMSOPCKFiles(String basename, List comments) throws SpiceException { + + ReferenceFrame instrFrame = new ReferenceFrame(config.instrumentFrameName()); + ReferenceFrame scFrame = new ReferenceFrame(config.spacecraftFrame()); + ReferenceFrame j2000 = new ReferenceFrame("J2000"); + ReferenceFrame bodyFixed = new ReferenceFrame(config.bodyFrame()); + + ReferenceFrame ref = config.J2000() ? j2000 : bodyFixed; + + logger.debug("Body fixed frame: {}", bodyFixed.getName()); + logger.debug("Instrument frame: {}", instrFrame.getName()); + logger.debug("Spacecraft frame: {}", scFrame.getName()); + logger.debug(" Reference frame: {}", ref.getName()); + + File commentFile = new File(basename + "-comments.txt"); + if (commentFile.exists()) + if (!commentFile.delete()) logger.error("{} exists but cannot be deleted!", commentFile.getPath()); + + String setupFile = basename + ".setup"; + String inputFile = basename + ".inp"; + + try (PrintWriter pw = new PrintWriter(commentFile)) { + StringBuilder sb = new StringBuilder(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss z"); + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); + + sb.append("This CK was created on ").append(dtf.format(now)).append(" from the following sumFiles:\n"); + for (SumFile sumFile : sumFiles.keySet()) { + sb.append(String.format("\t%s %s\n", sumFile.utcString(), sumFiles.get(sumFile))); + } + sb.append("\n"); + sb.append("providing the orientation of ") + .append(scFrame.getName()) + .append(" with respect to ") + .append(config.J2000() ? "J2000" : bodyFixed.getName()) + .append(". "); + double first = new TDBTime(sumFiles.firstKey().utcString()).getTDBSeconds(); + double last = new TDBTime(sumFiles.lastKey().utcString()).getTDBSeconds() + config.extend(); + sb.append("The coverage period is ") + .append(new TDBTime(first).toUTCString("ISOC", 3)) + .append(" to ") + .append(new TDBTime(last).toUTCString("ISOC", 3)) + .append(" UTC."); + + String allComments = sb.toString(); + for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + + Map map = new TreeMap<>(); + map.put("LSK_FILE_NAME", "'" + config.lsk() + "'"); + map.put("SCLK_FILE_NAME", "'" + config.sclk() + "'"); + map.put("CK_TYPE", "3"); + map.put("COMMENTS_FILE_NAME", String.format("'%s'", commentFile.getPath())); + map.put("INSTRUMENT_ID", String.format("%d", scFrame.getIDCode())); + map.put("REFERENCE_FRAME_NAME", String.format("'%s'", config.J2000() ? "J2000" : bodyFixed.getName())); + if (!config.fk().isEmpty()) map.put("FRAMES_FILE_NAME", "'" + config.fk() + "'"); + map.put("ANGULAR_RATE_PRESENT", "'MAKE UP/NO AVERAGING'"); + map.put("INPUT_TIME_TYPE", "'UTC'"); + map.put("INPUT_DATA_TYPE", "'SPICE QUATERNIONS'"); + map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); + + try (PrintWriter pw = new PrintWriter(setupFile)) { + pw.println("\\begindata"); + for (String key : map.keySet()) { + pw.printf("%s = %s\n", key, map.get(key)); + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + + NavigableMap attitudeMap = new TreeMap<>(); + for (SumFile s : sumFiles.keySet()) { + TDBTime t = new TDBTime(s.utcString()); + + Vector3[] rows = new Vector3[3]; + rows[0] = MathConversions.toVector3(s.cx()); + rows[1] = MathConversions.toVector3(s.cy()); + rows[2] = MathConversions.toVector3(s.cz()); + + Vector3 row0 = rows[Math.abs(config.flipX()) - 1]; + Vector3 row1 = rows[Math.abs(config.flipY()) - 1]; + Vector3 row2 = rows[Math.abs(config.flipZ()) - 1]; + + if (config.flipX() < 0) row0 = row0.negate(); + if (config.flipY() < 0) row1 = row1.negate(); + if (config.flipZ() < 0) row2 = row2.negate(); + + Matrix33 refToInstr = new Matrix33(row0, row1, row2); + + if (config.J2000()) { + Matrix33 j2000ToBodyFixed = j2000.getPositionTransformation(bodyFixed, t); + refToInstr = refToInstr.mxm(j2000ToBodyFixed); + } + + Matrix33 instrToSc = instrFrame.getPositionTransformation(scFrame, t); + Matrix33 refToSc = instrToSc.mxm(refToInstr); + + SpiceQuaternion q = new SpiceQuaternion(refToSc); + attitudeMap.put(t.getTDBSeconds(), q); + } + + if (config.extend() > 0) { + var lastEntry = attitudeMap.lastEntry(); + attitudeMap.put(lastEntry.getKey() + config.extend(), lastEntry.getValue()); + } + + try (PrintWriter pw = new PrintWriter(new FileWriter(inputFile))) { + for (double t : attitudeMap.keySet()) { + SpiceQuaternion q = attitudeMap.get(t); + pw.printf( + "%s %.14e %.14e %.14e %.14e\n", + new TDBTime(t).toUTCString("ISOC", 6), q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3)); + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + + return String.format("msopck %s %s %s.bc", setupFile, inputFile, basename); } - try (PrintWriter pw = new PrintWriter(new FileWriter(inputFile))) { - for (double t : attitudeMap.keySet()) { - SpiceQuaternion q = attitudeMap.get(t); - pw.printf("%s %.14e %.14e %.14e %.14e\n", new TDBTime(t).toUTCString("ISOC", 6), - q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3)); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); + public static void main(String[] args) throws SpiceException, IOException { + TerrasaurTool defaultOBJ = new CKFromSumFile(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + if (cl.hasOption("dumpConfig")) { + CKFromSumFileConfigFactory factory = new CKFromSumFileConfigFactory(); + PropertiesConfiguration config = factory.toConfig(factory.getTemplate()); + try { + String filename = cl.getOptionValue("dumpConfig"); + config.write(new PrintWriter(filename)); + logger.info("Wrote {}", filename); + } catch (ConfigurationException | IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + System.exit(0); + } + + NativeLibraryLoader.loadSpiceLibraries(); + + PropertiesConfiguration config = null; + CKFromSumFileConfigFactory factory = new CKFromSumFileConfigFactory(); + try { + config = new Configurations().properties(new File(cl.getOptionValue("config"))); + } catch (ConfigurationException e1) { + logger.error(e1.getLocalizedMessage(), e1); + } + + CKFromSumFileConfig appConfig = factory.fromConfig(config); + + for (String kernel : appConfig.metakernel()) KernelDatabase.load(kernel); + + NavigableMap sumFiles = new TreeMap<>((o1, o2) -> { + try { + return Double.compare( + new TDBTime(o1.utcString()).getTDBSeconds(), new TDBTime(o2.utcString()).getTDBSeconds()); + } catch (SpiceErrorException e) { + logger.error(e.getLocalizedMessage(), e); + } + return 0; + }); + + List lines = FileUtils.readLines(new File(cl.getOptionValue("sumFile")), Charset.defaultCharset()); + for (String line : lines) { + if (line.strip().startsWith("#")) continue; + String[] parts = line.strip().split("\\s+"); + String filename = parts[0].trim(); + sumFiles.put(SumFile.fromFile(new File(filename)), FilenameUtils.getBaseName(filename)); + } + + CKFromSumFile app = new CKFromSumFile(appConfig, sumFiles); + TDBTime begin = new TDBTime(sumFiles.firstKey().utcString()); + TDBTime end = new TDBTime(sumFiles.lastKey().utcString()); + String picture = "YYYY_DOY"; + String command = app.writeMSOPCKFiles( + String.format("ck_%s_%s", begin.toString(picture), end.toString(picture)), new ArrayList<>()); + + logger.info("To generate the CK, run:\n\t{}", command); + + logger.info("Finished."); } - - return String.format("msopck %s %s %s.bc", setupFile, inputFile, basename); - } - - public static void main(String[] args) throws SpiceException, IOException { - TerrasaurTool defaultOBJ = new CKFromSumFile(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - if (cl.hasOption("dumpConfig")){ - CKFromSumFileConfigFactory factory = new CKFromSumFileConfigFactory(); - PropertiesConfiguration config = factory.toConfig(factory.getTemplate()); - try { - String filename = cl.getOptionValue("dumpConfig"); - config.write(new PrintWriter(filename)); - logger.info("Wrote {}", filename); - } catch (ConfigurationException | IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - System.exit(0); - } - - NativeLibraryLoader.loadSpiceLibraries(); - - PropertiesConfiguration config = null; - CKFromSumFileConfigFactory factory = new CKFromSumFileConfigFactory(); - try { - config = new Configurations().properties(new File(cl.getOptionValue("config"))); - } catch (ConfigurationException e1) { - logger.error(e1.getLocalizedMessage(), e1); - } - - CKFromSumFileConfig appConfig = factory.fromConfig(config); - - for (String kernel : appConfig.metakernel()) - KernelDatabase.load(kernel); - - NavigableMap sumFiles = new TreeMap<>((o1, o2) -> { - try { - return Double.compare(new TDBTime(o1.utcString()).getTDBSeconds(), - new TDBTime(o2.utcString()).getTDBSeconds()); - } catch (SpiceErrorException e) { - logger.error(e.getLocalizedMessage(), e); - } - return 0; - }); - - List lines = - FileUtils.readLines(new File(cl.getOptionValue("sumFile")), Charset.defaultCharset()); - for (String line : lines) { - if (line.strip().startsWith("#")) - continue; - String[] parts = line.strip().split("\\s+"); - String filename = parts[0].trim(); - sumFiles.put(SumFile.fromFile(new File(filename)), FilenameUtils.getBaseName(filename)); - } - - CKFromSumFile app = new CKFromSumFile(appConfig, sumFiles); - TDBTime begin = new TDBTime(sumFiles.firstKey().utcString()); - TDBTime end = new TDBTime(sumFiles.lastKey().utcString()); - String picture = "YYYY_DOY"; - String command = app.writeMSOPCKFiles( - String.format("ck_%s_%s", begin.toString(picture), end.toString(picture)), - new ArrayList<>()); - - logger.info("To generate the CK, run:\n\t{}", command); - - logger.info("Finished."); - - } - } diff --git a/src/main/java/terrasaur/apps/ColorSpots.java b/src/main/java/terrasaur/apps/ColorSpots.java index 1c02521..fb40cf8 100644 --- a/src/main/java/terrasaur/apps/ColorSpots.java +++ b/src/main/java/terrasaur/apps/ColorSpots.java @@ -59,531 +59,511 @@ import vtk.vtkPolyData; */ public class ColorSpots implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private ColorSpots() {} + private ColorSpots() {} - @Override - public String shortDescription() { - return "Assign values to facets in a shape model from an input dataset."; - } + @Override + public String shortDescription() { + return "Assign values to facets in a shape model from an input dataset."; + } - @Override - public String fullDescription(Options options) { + @Override + public String fullDescription(Options options) { - String header = ""; - String footer = - """ + String header = ""; + String footer = + """ This program reads an OBJ file along with a CSV file containing locations and values and writes the \ mean value and standard deviation for each facet within a specified distance of an input point to standard \ out. Latitude and longitude are specified in degrees. Longitude is east longitude. Units of x, y, z, and \ radius are the same as the units in the supplied OBJ file. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private enum FORMAT { - LL, - LLR, - XYZ - } - - private enum FIELD { - MIN, - MAX, - MEDIAN, - N, - RMS, - SUM, - STD, - VARIANCE - } - - private vtkPolyData polyData; - private SmallBodyModel smallBodyModel; - - public ColorSpots(vtkPolyData polyData) { - this.polyData = polyData; - this.smallBodyModel = new SmallBodyModel(polyData); - } - - private long getXYZ(double lat, double lon, double[] pt) { - double[] origin = {0., 0., 0.}; - Vector3D lookDir = new Vector3D(lon, lat); - - return smallBodyModel.computeRayIntersection(origin, lookDir.toArray(), pt); - } - - private ArrayList readCSV(String filename, FORMAT format) { - - ArrayList returnArray = new ArrayList<>(); - - try (Reader in = new FileReader(filename)) { - Iterable records = CSVFormat.DEFAULT.parse(in); - for (CSVRecord record : records) { - double[] values = new double[4]; - values[3] = Double.NaN; - if (format == FORMAT.LL) { - double lon = Math.toRadians(Double.parseDouble(record.get(0).trim())); - double lat = Math.toRadians(Double.parseDouble(record.get(1).trim())); - - if (getXYZ(lat, lon, values) < 0) continue; - try { - values[3] = Double.parseDouble(record.get(2)); - } catch (NumberFormatException e) { - continue; - } - } else { - if (format == FORMAT.LLR) { - double lon = Math.toRadians(Double.parseDouble(record.get(0).trim())); - double lat = Math.toRadians(Double.parseDouble(record.get(1).trim())); - double rad = Double.parseDouble(record.get(2).trim()); - Vector3D xyz = new Vector3D(lon, lat).scalarMultiply(rad); - values[0] = xyz.getX(); - values[1] = xyz.getY(); - values[2] = xyz.getZ(); - } else if (format == FORMAT.XYZ) { - values[0] = Double.parseDouble(record.get(0).trim()); - values[1] = Double.parseDouble(record.get(1).trim()); - values[2] = Double.parseDouble(record.get(2).trim()); - } - smallBodyModel.findClosestCell(values); - try { - values[3] = Double.parseDouble(record.get(3).trim()); - } catch (NumberFormatException e) { - continue; - } - } - returnArray.add(values); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); + return TerrasaurTool.super.fullDescription(options, header, footer); } - return returnArray; - } - - public TreeMap getStatsFast( - ArrayList valuesList, double radius, boolean weight, boolean atVertices) { - return atVertices - ? getStatsVertex(valuesList, radius, weight) - : getStatsFacet(valuesList, radius, weight); - } - - private TreeMap getStatsVertex( - ArrayList valuesList, double radius, boolean weight) { - - TreeMap statMap = new TreeMap<>(); - for (long i = 0; i < smallBodyModel.getSmallBodyPolyData().GetNumberOfPoints(); i++) { - DescriptiveStatistics stats = new DescriptiveStatistics(); - statMap.put(i, stats); - } - // double[] xyz = new double[3]; - - for (double[] values : valuesList) { - Vector3D xyz = new Vector3D(values[0], values[1], values[2]); - double value = values[3]; - - vtkIdList pointIDs = new vtkIdList(); - smallBodyModel.getPointLocator().FindPointsWithinRadius(radius, xyz.toArray(), pointIDs); - // pointIDs.InsertNextId(smallBodyModel.getPointLocator().FindClosestPoint(xyz)); - - for (int i = 0; i < pointIDs.GetNumberOfIds(); i++) { - long pointID = pointIDs.GetId(i); - DescriptiveStatistics stats = statMap.get(pointID); - - Vector3D p = new Vector3D(smallBodyModel.getSmallBodyPolyData().GetPoint(pointID)); - double dist = p.distance(xyz); - - // cell center can be farther than radius as long as one point is closer than // - // radius - if (dist < radius) { - double thisValue = value; - if (weight) thisValue *= (1 - dist / radius); - stats.addValue(thisValue); - - // if (thisValue < 0) - // System.out.printf("Cell %d dist %f radius %f xyz %f %f %f value %f thisValue %e\n", - // cellID, dist, radius, - // xyz[0], xyz[1], xyz[2], - // value, thisValue); - } - } // point loop - } // values loop - - return statMap; - } - - private TreeMap getStatsFacet( - ArrayList valuesList, double radius, boolean weight) { - - TreeMap statMap = new TreeMap<>(); - for (long i = 0; i < smallBodyModel.getSmallBodyPolyData().GetNumberOfCells(); i++) { - DescriptiveStatistics stats = new DescriptiveStatistics(); - statMap.put(i, stats); + private enum FORMAT { + LL, + LLR, + XYZ } - for (double[] values : valuesList) { - Vector3D xyz = new Vector3D(values[0], values[1], values[2]); - double value = values[3]; - - Set cellIDs = smallBodyModel.findClosestCellsWithinRadius(xyz.toArray(), radius); - // cellIDs.add(smallBodyModel.findClosestCell(xyz)); - - for (Long cellID : cellIDs) { - DescriptiveStatistics stats = statMap.get(cellID); - - TriangularFacet tf = PolyDataUtil.getFacet(polyData, cellID); - Vector3D p = MathConversions.toVector3D(tf.getCenter()); - double dist = p.distance(xyz); - - // cell center can be farther than radius as long as one point is closer than // - // radius - if (dist < radius) { - double thisValue = value; - if (weight) thisValue *= (1 - dist / radius); - stats.addValue(thisValue); - - // if (thisValue < 0) - // System.out.printf("Cell %d dist %f radius %f xyz %f %f %f value %f thisValue %e\n", - // cellID, dist, radius, - // xyz[0], xyz[1], xyz[2], - // value, thisValue); - } - } // cell loop - } // values loop - - return statMap; - } - - public TreeMap getStats( - ArrayList valuesList, double radius) { - - // for each value, store indices of closest cells and distances - TreeMap>> closestCells = new TreeMap<>(); - for (int i = 0; i < valuesList.size(); i++) { - double[] values = valuesList.get(i); - Vector3D xyz = new Vector3D(values); - - TreeSet sortedCellIDs = - new TreeSet<>(smallBodyModel.findClosestCellsWithinRadius(values, radius)); - sortedCellIDs.add(smallBodyModel.findClosestCell(values)); - - ArrayList> distances = new ArrayList<>(); - for (long cellID : sortedCellIDs) { - TriangularFacet tf = PolyDataUtil.getFacet(polyData, cellID); - Vector3D p = MathConversions.toVector3D(tf.getCenter()); - double dist = p.distance(xyz); - distances.add(Pair.create(cellID, dist)); - } - closestCells.put(i, distances); + private enum FIELD { + MIN, + MAX, + MEDIAN, + N, + RMS, + SUM, + STD, + VARIANCE } - TreeMap statMap = new TreeMap<>(); - for (int cellID = 0; cellID < polyData.GetNumberOfCells(); cellID++) { - DescriptiveStatistics stats = statMap.get(cellID); - if (stats == null) { - stats = new DescriptiveStatistics(); - statMap.put(cellID, stats); - } + private vtkPolyData polyData; + private SmallBodyModel smallBodyModel; - for (int i = 0; i < valuesList.size(); i++) { - double[] values = valuesList.get(i); + public ColorSpots(vtkPolyData polyData) { + this.polyData = polyData; + this.smallBodyModel = new SmallBodyModel(polyData); + } - ArrayList> distances = closestCells.get(i); - for (Pair pair : distances) { + private long getXYZ(double lat, double lon, double[] pt) { + double[] origin = {0., 0., 0.}; + Vector3D lookDir = new Vector3D(lon, lat); - if (pair.getFirst().intValue() < cellID) continue; + return smallBodyModel.computeRayIntersection(origin, lookDir.toArray(), pt); + } - if (pair.getFirst().intValue() > cellID) break; + private ArrayList readCSV(String filename, FORMAT format) { - if (pair.getFirst().intValue() == cellID) { - double dist = pair.getSecond(); - if (dist < radius) { - double thisValue = (1 - dist / radius) * values[3]; - stats.addValue(thisValue); + ArrayList returnArray = new ArrayList<>(); + + try (Reader in = new FileReader(filename)) { + Iterable records = CSVFormat.DEFAULT.parse(in); + for (CSVRecord record : records) { + double[] values = new double[4]; + values[3] = Double.NaN; + if (format == FORMAT.LL) { + double lon = Math.toRadians(Double.parseDouble(record.get(0).trim())); + double lat = Math.toRadians(Double.parseDouble(record.get(1).trim())); + + if (getXYZ(lat, lon, values) < 0) continue; + try { + values[3] = Double.parseDouble(record.get(2)); + } catch (NumberFormatException e) { + continue; + } + } else { + if (format == FORMAT.LLR) { + double lon = + Math.toRadians(Double.parseDouble(record.get(0).trim())); + double lat = + Math.toRadians(Double.parseDouble(record.get(1).trim())); + double rad = Double.parseDouble(record.get(2).trim()); + Vector3D xyz = new Vector3D(lon, lat).scalarMultiply(rad); + values[0] = xyz.getX(); + values[1] = xyz.getY(); + values[2] = xyz.getZ(); + } else if (format == FORMAT.XYZ) { + values[0] = Double.parseDouble(record.get(0).trim()); + values[1] = Double.parseDouble(record.get(1).trim()); + values[2] = Double.parseDouble(record.get(2).trim()); + } + smallBodyModel.findClosestCell(values); + try { + values[3] = Double.parseDouble(record.get(3).trim()); + } catch (NumberFormatException e) { + continue; + } + } + returnArray.add(values); } - } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); } - } - } // cell loop - return statMap; - } - - public static void main(String[] args) { - // run the VTK garbage collector every 30 seconds - vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetScheduleTime(30, TimeUnit.SECONDS); - vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetAutoGarbageCollection(true); - // vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetDebug(true); - - try { - ColorSpotsMain(args); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); + return returnArray; } - vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetAutoGarbageCollection(false); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("additionalFields") - .hasArg() - .desc( - "Specify additional fields to write out. Allowed values are min, max, median, n, rms, sum, std, variance. " - + "More than one field may be specified in a comma separated list (e.g. " - + "-additionalFields sum,median,rms). Additional fields will be written out after the mean and std columns.") - .build()); - options.addOption( - Option.builder("allFacets") - .desc( - "Report values for all facets in OBJ shape model, even if facet is not within searchRadius " - + "of any points. Prints NaN if facet not within searchRadius. Default is to only " - + "print facets which have contributions from input points.") - .build()); - options.addOption( - Option.builder("info") - .required() - .hasArg() - .desc( - "Required. Name of CSV file containing value to plot." - + " Default format is lon, lat, radius, value. See -xyz and -llOnly options for alternate formats.") - .build()); - options.addOption( - Option.builder("llOnly").desc("Format of -info file is lon, lat, value.").build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("normalize") - .desc( - "Report values per unit area (divide by total area of facets within search ellipse).") - .build()); - options.addOption( - Option.builder("noWeight") - .desc("Do not weight points by distance from facet/vertex.") - .build()); - options.addOption( - Option.builder("obj") - .required() - .hasArg() - .desc("Required. Name of shape model to read.") - .build()); - options.addOption( - Option.builder("outFile") - .hasArg() - .desc("Specify output file to store the output.") - .build()); - options.addOption( - Option.builder("searchRadius") - .hasArg() - .desc( - "Each facet will be colored using a weighted average of all points within searchRadius of the facet/vertex. " - + "If not present, set to sqrt(2)/2 * mean facet edge length.") - .build()); - options.addOption( - Option.builder("writeVertices") - .desc( - "Convert output from a per facet to per vertex format. Each line will be of the form" - + " x, y, z, value, sigma where x, y, z are the vector components of vertex V. " - + " Default is to only report facetID, facet_value, facet_sigma.") - .build()); - options.addOption( - Option.builder("xyz").desc("Format of -info file is x, y, z, value.").build()); - return options; - } - - public static void ColorSpotsMain(String[] args) throws Exception { - - TerrasaurTool defaultOBJ = new ColorSpots(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadVtkLibraries(); - - final boolean writeVerts = cl.hasOption("writeVertices"); - final boolean allFacets = cl.hasOption("allFacets"); - final boolean normalize = cl.hasOption("normalize") && !writeVerts; - final boolean weight = !cl.hasOption("noWeight"); - FORMAT format = FORMAT.LLR; - for (Option option : cl.getOptions()) { - if (option.getOpt().equals("xyz")) { - format = FORMAT.XYZ; - } - if (option.getOpt().equals("llOnly")) { - format = FORMAT.LL; - } + public TreeMap getStatsFast( + ArrayList valuesList, double radius, boolean weight, boolean atVertices) { + return atVertices ? getStatsVertex(valuesList, radius, weight) : getStatsFacet(valuesList, radius, weight); } - vtkPolyData polyData = PolyDataUtil.loadShapeModelAndComputeNormals(cl.getOptionValue("obj")); + private TreeMap getStatsVertex( + ArrayList valuesList, double radius, boolean weight) { - double radius; - if (cl.hasOption("searchRadius")) { - radius = Double.parseDouble(cl.getOptionValue("searchRadius")); - } else { - PolyDataStatistics stats = new PolyDataStatistics(polyData); - radius = stats.getMeanEdgeLength() * Math.sqrt(2) / 2; - logger.info("Using search radius of " + radius); - } - - ColorSpots cs = new ColorSpots(polyData); - ArrayList infoValues = cs.readCSV(cl.getOptionValue("info"), format); - TreeMap statMap = - cs.getStatsFast(infoValues, radius, weight, writeVerts); - - double totalArea = 0; - if (normalize) { - for (int facet = 0; facet < polyData.GetNumberOfCells(); facet++) { - vtkCell cell = polyData.GetCell(facet); - vtkPoints points = cell.GetPoints(); - double[] pt0 = points.GetPoint(0); - double[] pt1 = points.GetPoint(1); - double[] pt2 = points.GetPoint(2); - - TriangularFacet tf = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - double area = tf.getArea(); - - totalArea += area; - points.Delete(); - cell.Delete(); - } - } - - ArrayList fields = new ArrayList<>(); - if (cl.hasOption("additionalFields")) { - for (String s : cl.getOptionValue("additionalFields").trim().toUpperCase().split(",")) { - for (FIELD f : FIELD.values()) { - if (f.name().equalsIgnoreCase(s)) fields.add(f); + TreeMap statMap = new TreeMap<>(); + for (long i = 0; i < smallBodyModel.getSmallBodyPolyData().GetNumberOfPoints(); i++) { + DescriptiveStatistics stats = new DescriptiveStatistics(); + statMap.put(i, stats); } - } + // double[] xyz = new double[3]; + + for (double[] values : valuesList) { + Vector3D xyz = new Vector3D(values[0], values[1], values[2]); + double value = values[3]; + + vtkIdList pointIDs = new vtkIdList(); + smallBodyModel.getPointLocator().FindPointsWithinRadius(radius, xyz.toArray(), pointIDs); + // pointIDs.InsertNextId(smallBodyModel.getPointLocator().FindClosestPoint(xyz)); + + for (int i = 0; i < pointIDs.GetNumberOfIds(); i++) { + long pointID = pointIDs.GetId(i); + DescriptiveStatistics stats = statMap.get(pointID); + + Vector3D p = new Vector3D(smallBodyModel.getSmallBodyPolyData().GetPoint(pointID)); + double dist = p.distance(xyz); + + // cell center can be farther than radius as long as one point is closer than // + // radius + if (dist < radius) { + double thisValue = value; + if (weight) thisValue *= (1 - dist / radius); + stats.addValue(thisValue); + + // if (thisValue < 0) + // System.out.printf("Cell %d dist %f radius %f xyz %f %f %f value %f thisValue %e\n", + // cellID, dist, radius, + // xyz[0], xyz[1], xyz[2], + // value, thisValue); + } + } // point loop + } // values loop + + return statMap; } - TreeMap> map = new TreeMap<>(); - long numPoints = (writeVerts ? polyData.GetNumberOfPoints() : polyData.GetNumberOfCells()); - for (long index = 0; index < numPoints; index++) { - DescriptiveStatistics stats = statMap.get(index); - ArrayList values = new ArrayList<>(); - if (stats != null) { - values.add(stats.getMean()); - values.add(stats.getStandardDeviation()); - for (FIELD f : fields) { - if (f == FIELD.MIN) values.add(stats.getMin()); - if (f == FIELD.MAX) values.add(stats.getMax()); - if (f == FIELD.MEDIAN) values.add(stats.getPercentile(50)); - if (f == FIELD.N) values.add((double) stats.getN()); - if (f == FIELD.RMS) values.add(Math.sqrt(stats.getSumsq() / stats.getN())); - if (f == FIELD.STD) values.add(stats.getStandardDeviation()); - if (f == FIELD.SUM) values.add(stats.getSum()); - if (f == FIELD.VARIANCE) values.add(stats.getVariance()); + private TreeMap getStatsFacet( + ArrayList valuesList, double radius, boolean weight) { + + TreeMap statMap = new TreeMap<>(); + for (long i = 0; i < smallBodyModel.getSmallBodyPolyData().GetNumberOfCells(); i++) { + DescriptiveStatistics stats = new DescriptiveStatistics(); + statMap.put(i, stats); } - } else { - values.add(Double.NaN); - values.add(Double.NaN); - for (FIELD f : fields) { - if (f == FIELD.MIN) values.add(Double.NaN); - if (f == FIELD.MAX) values.add(Double.NaN); - if (f == FIELD.MEDIAN) values.add(Double.NaN); - if (f == FIELD.N) values.add(Double.NaN); - if (f == FIELD.RMS) values.add(Double.NaN); - if (f == FIELD.STD) values.add(Double.NaN); - if (f == FIELD.SUM) values.add(Double.NaN); - if (f == FIELD.VARIANCE) values.add(Double.NaN); + + for (double[] values : valuesList) { + Vector3D xyz = new Vector3D(values[0], values[1], values[2]); + double value = values[3]; + + Set cellIDs = smallBodyModel.findClosestCellsWithinRadius(xyz.toArray(), radius); + // cellIDs.add(smallBodyModel.findClosestCell(xyz)); + + for (Long cellID : cellIDs) { + DescriptiveStatistics stats = statMap.get(cellID); + + TriangularFacet tf = PolyDataUtil.getFacet(polyData, cellID); + Vector3D p = MathConversions.toVector3D(tf.getCenter()); + double dist = p.distance(xyz); + + // cell center can be farther than radius as long as one point is closer than // + // radius + if (dist < radius) { + double thisValue = value; + if (weight) thisValue *= (1 - dist / radius); + stats.addValue(thisValue); + + // if (thisValue < 0) + // System.out.printf("Cell %d dist %f radius %f xyz %f %f %f value %f thisValue %e\n", + // cellID, dist, radius, + // xyz[0], xyz[1], xyz[2], + // value, thisValue); + } + } // cell loop + } // values loop + + return statMap; + } + + public TreeMap getStats(ArrayList valuesList, double radius) { + + // for each value, store indices of closest cells and distances + TreeMap>> closestCells = new TreeMap<>(); + for (int i = 0; i < valuesList.size(); i++) { + double[] values = valuesList.get(i); + Vector3D xyz = new Vector3D(values); + + TreeSet sortedCellIDs = new TreeSet<>(smallBodyModel.findClosestCellsWithinRadius(values, radius)); + sortedCellIDs.add(smallBodyModel.findClosestCell(values)); + + ArrayList> distances = new ArrayList<>(); + for (long cellID : sortedCellIDs) { + TriangularFacet tf = PolyDataUtil.getFacet(polyData, cellID); + Vector3D p = MathConversions.toVector3D(tf.getCenter()); + double dist = p.distance(xyz); + distances.add(Pair.create(cellID, dist)); + } + closestCells.put(i, distances); } - } - map.put(index, values); + + TreeMap statMap = new TreeMap<>(); + for (int cellID = 0; cellID < polyData.GetNumberOfCells(); cellID++) { + DescriptiveStatistics stats = statMap.get(cellID); + if (stats == null) { + stats = new DescriptiveStatistics(); + statMap.put(cellID, stats); + } + + for (int i = 0; i < valuesList.size(); i++) { + double[] values = valuesList.get(i); + + ArrayList> distances = closestCells.get(i); + for (Pair pair : distances) { + + if (pair.getFirst().intValue() < cellID) continue; + + if (pair.getFirst().intValue() > cellID) break; + + if (pair.getFirst().intValue() == cellID) { + double dist = pair.getSecond(); + if (dist < radius) { + double thisValue = (1 - dist / radius) * values[3]; + stats.addValue(thisValue); + } + } + } + } + } // cell loop + + return statMap; } - ArrayList returnList; + public static void main(String[] args) { + // run the VTK garbage collector every 30 seconds + vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetScheduleTime(30, TimeUnit.SECONDS); + vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetAutoGarbageCollection(true); + // vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetDebug(true); - if (writeVerts) { - returnList = writeVertices(map, polyData, allFacets); - } else { - returnList = writeFacets(map, allFacets, normalize, totalArea); + try { + ColorSpotsMain(args); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + + vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetAutoGarbageCollection(false); } - if (cl.hasOption("outFile")) { - try (PrintWriter pw = new PrintWriter(cl.getOptionValue("outFile"))) { - for (String s : returnList) pw.println(s); - } - } else { - for (String string : returnList) System.out.println(string); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("additionalFields") + .hasArg() + .desc( + "Specify additional fields to write out. Allowed values are min, max, median, n, rms, sum, std, variance. " + + "More than one field may be specified in a comma separated list (e.g. " + + "-additionalFields sum,median,rms). Additional fields will be written out after the mean and std columns.") + .build()); + options.addOption(Option.builder("allFacets") + .desc("Report values for all facets in OBJ shape model, even if facet is not within searchRadius " + + "of any points. Prints NaN if facet not within searchRadius. Default is to only " + + "print facets which have contributions from input points.") + .build()); + options.addOption(Option.builder("info") + .required() + .hasArg() + .desc( + "Required. Name of CSV file containing value to plot." + + " Default format is lon, lat, radius, value. See -xyz and -llOnly options for alternate formats.") + .build()); + options.addOption(Option.builder("llOnly") + .desc("Format of -info file is lon, lat, value.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("normalize") + .desc("Report values per unit area (divide by total area of facets within search ellipse).") + .build()); + options.addOption(Option.builder("noWeight") + .desc("Do not weight points by distance from facet/vertex.") + .build()); + options.addOption(Option.builder("obj") + .required() + .hasArg() + .desc("Required. Name of shape model to read.") + .build()); + options.addOption(Option.builder("outFile") + .hasArg() + .desc("Specify output file to store the output.") + .build()); + options.addOption(Option.builder("searchRadius") + .hasArg() + .desc( + "Each facet will be colored using a weighted average of all points within searchRadius of the facet/vertex. " + + "If not present, set to sqrt(2)/2 * mean facet edge length.") + .build()); + options.addOption(Option.builder("writeVertices") + .desc("Convert output from a per facet to per vertex format. Each line will be of the form" + + " x, y, z, value, sigma where x, y, z are the vector components of vertex V. " + + " Default is to only report facetID, facet_value, facet_sigma.") + .build()); + options.addOption(Option.builder("xyz") + .desc("Format of -info file is x, y, z, value.") + .build()); + return options; } - } - private static ArrayList writeFacets( - TreeMap> map, - boolean allFacets, - boolean normalize, - double totalArea) { + public static void ColorSpotsMain(String[] args) throws Exception { - ArrayList returnList = new ArrayList<>(); + TerrasaurTool defaultOBJ = new ColorSpots(); - for (Long facet : map.keySet()) { - ArrayList values = map.get(facet); - Double value = values.get(0); - Double sigma = values.get(1); - if (allFacets || !value.isNaN()) { + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + NativeLibraryLoader.loadVtkLibraries(); + + final boolean writeVerts = cl.hasOption("writeVertices"); + final boolean allFacets = cl.hasOption("allFacets"); + final boolean normalize = cl.hasOption("normalize") && !writeVerts; + final boolean weight = !cl.hasOption("noWeight"); + FORMAT format = FORMAT.LLR; + for (Option option : cl.getOptions()) { + if (option.getOpt().equals("xyz")) { + format = FORMAT.XYZ; + } + if (option.getOpt().equals("llOnly")) { + format = FORMAT.LL; + } + } + + vtkPolyData polyData = PolyDataUtil.loadShapeModelAndComputeNormals(cl.getOptionValue("obj")); + + double radius; + if (cl.hasOption("searchRadius")) { + radius = Double.parseDouble(cl.getOptionValue("searchRadius")); + } else { + PolyDataStatistics stats = new PolyDataStatistics(polyData); + radius = stats.getMeanEdgeLength() * Math.sqrt(2) / 2; + logger.info("Using search radius of " + radius); + } + + ColorSpots cs = new ColorSpots(polyData); + ArrayList infoValues = cs.readCSV(cl.getOptionValue("info"), format); + TreeMap statMap = cs.getStatsFast(infoValues, radius, weight, writeVerts); + + double totalArea = 0; if (normalize) { - value /= totalArea; - sigma /= totalArea; + for (int facet = 0; facet < polyData.GetNumberOfCells(); facet++) { + vtkCell cell = polyData.GetCell(facet); + vtkPoints points = cell.GetPoints(); + double[] pt0 = points.GetPoint(0); + double[] pt1 = points.GetPoint(1); + double[] pt2 = points.GetPoint(2); + + TriangularFacet tf = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + double area = tf.getArea(); + + totalArea += area; + points.Delete(); + cell.Delete(); + } } - StringBuilder sb = new StringBuilder(String.format("%d, %e, %e", facet, value, sigma)); - for (int i = 2; i < values.size(); i++) { - value = values.get(i); - if (normalize) value /= totalArea; - sb.append(String.format(", %e", value)); + + ArrayList fields = new ArrayList<>(); + if (cl.hasOption("additionalFields")) { + for (String s : + cl.getOptionValue("additionalFields").trim().toUpperCase().split(",")) { + for (FIELD f : FIELD.values()) { + if (f.name().equalsIgnoreCase(s)) fields.add(f); + } + } + } + + TreeMap> map = new TreeMap<>(); + long numPoints = (writeVerts ? polyData.GetNumberOfPoints() : polyData.GetNumberOfCells()); + for (long index = 0; index < numPoints; index++) { + DescriptiveStatistics stats = statMap.get(index); + ArrayList values = new ArrayList<>(); + if (stats != null) { + values.add(stats.getMean()); + values.add(stats.getStandardDeviation()); + for (FIELD f : fields) { + if (f == FIELD.MIN) values.add(stats.getMin()); + if (f == FIELD.MAX) values.add(stats.getMax()); + if (f == FIELD.MEDIAN) values.add(stats.getPercentile(50)); + if (f == FIELD.N) values.add((double) stats.getN()); + if (f == FIELD.RMS) values.add(Math.sqrt(stats.getSumsq() / stats.getN())); + if (f == FIELD.STD) values.add(stats.getStandardDeviation()); + if (f == FIELD.SUM) values.add(stats.getSum()); + if (f == FIELD.VARIANCE) values.add(stats.getVariance()); + } + } else { + values.add(Double.NaN); + values.add(Double.NaN); + for (FIELD f : fields) { + if (f == FIELD.MIN) values.add(Double.NaN); + if (f == FIELD.MAX) values.add(Double.NaN); + if (f == FIELD.MEDIAN) values.add(Double.NaN); + if (f == FIELD.N) values.add(Double.NaN); + if (f == FIELD.RMS) values.add(Double.NaN); + if (f == FIELD.STD) values.add(Double.NaN); + if (f == FIELD.SUM) values.add(Double.NaN); + if (f == FIELD.VARIANCE) values.add(Double.NaN); + } + } + map.put(index, values); + } + + ArrayList returnList; + + if (writeVerts) { + returnList = writeVertices(map, polyData, allFacets); + } else { + returnList = writeFacets(map, allFacets, normalize, totalArea); + } + + if (cl.hasOption("outFile")) { + try (PrintWriter pw = new PrintWriter(cl.getOptionValue("outFile"))) { + for (String s : returnList) pw.println(s); + } + } else { + for (String string : returnList) System.out.println(string); } - returnList.add(sb.toString()); - } } - return returnList; - } - private static ArrayList writeVertices( - TreeMap> map, vtkPolyData polyData, boolean allFacets) { + private static ArrayList writeFacets( + TreeMap> map, boolean allFacets, boolean normalize, double totalArea) { - ArrayList returnList = new ArrayList<>(); + ArrayList returnList = new ArrayList<>(); - double[] thisPt = new double[3]; - for (Long vertex : map.keySet()) { - ArrayList values = map.get(vertex); - Double value = values.get(0); - Double sigma = values.get(1); - if (allFacets || !value.isNaN()) { - // get vertex x,y,z values - polyData.GetPoint(vertex, thisPt); - StringBuilder sb = - new StringBuilder( - String.format("%e, %e, %e, %e, %e", thisPt[0], thisPt[1], thisPt[2], value, sigma)); - for (int i = 2; i < values.size(); i++) { - value = values.get(i); - sb.append(String.format(", %e", value)); + for (Long facet : map.keySet()) { + ArrayList values = map.get(facet); + Double value = values.get(0); + Double sigma = values.get(1); + if (allFacets || !value.isNaN()) { + if (normalize) { + value /= totalArea; + sigma /= totalArea; + } + StringBuilder sb = new StringBuilder(String.format("%d, %e, %e", facet, value, sigma)); + for (int i = 2; i < values.size(); i++) { + value = values.get(i); + if (normalize) value /= totalArea; + sb.append(String.format(", %e", value)); + } + returnList.add(sb.toString()); + } } - returnList.add(sb.toString()); - } + return returnList; + } + + private static ArrayList writeVertices( + TreeMap> map, vtkPolyData polyData, boolean allFacets) { + + ArrayList returnList = new ArrayList<>(); + + double[] thisPt = new double[3]; + for (Long vertex : map.keySet()) { + ArrayList values = map.get(vertex); + Double value = values.get(0); + Double sigma = values.get(1); + if (allFacets || !value.isNaN()) { + // get vertex x,y,z values + polyData.GetPoint(vertex, thisPt); + StringBuilder sb = new StringBuilder( + String.format("%e, %e, %e, %e, %e", thisPt[0], thisPt[1], thisPt[2], value, sigma)); + for (int i = 2; i < values.size(); i++) { + value = values.get(i); + sb.append(String.format(", %e", value)); + } + returnList.add(sb.toString()); + } + } + return returnList; } - return returnList; - } } diff --git a/src/main/java/terrasaur/apps/CompareOBJ.java b/src/main/java/terrasaur/apps/CompareOBJ.java index 0008434..8838e3b 100644 --- a/src/main/java/terrasaur/apps/CompareOBJ.java +++ b/src/main/java/terrasaur/apps/CompareOBJ.java @@ -57,978 +57,956 @@ import vtk.vtkUnstructuredGrid; public class CompareOBJ implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private CompareOBJ() {} + private CompareOBJ() {} - @Override - public String shortDescription() { - return "Report the differences between two OBJ shape files."; - } - - @Override - public String fullDescription(Options options) { - HelpFormatter formatter = new HelpFormatter(); - - String header = ""; - StringBuffer p1 = new StringBuffer("This program takes a point cloud or shape model and "); - p1.append("compares to a reference shape model."); - StringBuffer p2 = new StringBuffer("It iterates over each point or facet center and "); - p2.append("finds the closest point on the surface defined by the reference shape. "); - p2.append("The program then outputs the overall mean distance, mean squared distance "); - p2.append("and RMS distance between the corresponding points and prints them out to "); - p2.append("the terminal. The models do not need to be the same size. All units shown "); - p2.append("are in terms of the original units employed in the shape models (both must "); - p2.append("be the same). When comparing global models, use --fit-plane-radius for best "); - p2.append("results."); - - StringBuffer footer = new StringBuffer("\n"); - footer.append(WordUtils.wrap(p1.toString(), formatter.getWidth())); - footer.append("\n"); - footer.append("\n"); - footer.append(WordUtils.wrap(p2.toString(), formatter.getWidth())); - return TerrasaurTool.super.fullDescription(options, header, footer.toString()); - } - - private String referenceModelName; - private vtkPolyData polyDataModel; - private FORMATS inputFormat; - private vtkPolyData polyDataTruth; - - private LidarTransformation transform = LidarTransformation.defaultTransform(); - - private String tmpdir; - - private static class DistanceContainer implements Comparable { - double closestDistance; - double normalDistance; - - public DistanceContainer(double closestDistance) { - super(); - this.closestDistance = closestDistance; - this.normalDistance = -1; - } - - public void setNormalDistance(double normalDistance) { - this.normalDistance = normalDistance; - } - - public double getClosestDistance() { - return closestDistance; - } - - public double getNormalDistance() { - return normalDistance; + @Override + public String shortDescription() { + return "Report the differences between two OBJ shape files."; } @Override - public int compareTo(DistanceContainer o) { - return Double.compare(closestDistance, o.closestDistance); - } - } + public String fullDescription(Options options) { + HelpFormatter formatter = new HelpFormatter(); - public CompareOBJ(String modelName, String referenceModelName) { - this.referenceModelName = referenceModelName; - try { - inputFormat = FORMATS.formatFromExtension(modelName); - if (inputFormat.pointsOnly) { - vtkPoints points = PointCloudFormatConverter.readPointCloud(modelName); - polyDataModel = new vtkPolyData(); - polyDataModel.SetPoints(points); - } else polyDataModel = PolyDataUtil.loadShapeModel(modelName); - polyDataTruth = PolyDataUtil.loadShapeModel(referenceModelName); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - System.exit(0); + String header = ""; + StringBuffer p1 = new StringBuffer("This program takes a point cloud or shape model and "); + p1.append("compares to a reference shape model."); + StringBuffer p2 = new StringBuffer("It iterates over each point or facet center and "); + p2.append("finds the closest point on the surface defined by the reference shape. "); + p2.append("The program then outputs the overall mean distance, mean squared distance "); + p2.append("and RMS distance between the corresponding points and prints them out to "); + p2.append("the terminal. The models do not need to be the same size. All units shown "); + p2.append("are in terms of the original units employed in the shape models (both must "); + p2.append("be the same). When comparing global models, use --fit-plane-radius for best "); + p2.append("results."); + + StringBuffer footer = new StringBuffer("\n"); + footer.append(WordUtils.wrap(p1.toString(), formatter.getWidth())); + footer.append("\n"); + footer.append("\n"); + footer.append(WordUtils.wrap(p2.toString(), formatter.getWidth())); + return TerrasaurTool.super.fullDescription(options, header, footer.toString()); } - this.tmpdir = "."; - } + private String referenceModelName; + private vtkPolyData polyDataModel; + private FORMATS inputFormat; + private vtkPolyData polyDataTruth; - public void setTmpdir(String tmpdir) { - this.tmpdir = tmpdir; - } + private LidarTransformation transform = LidarTransformation.defaultTransform(); - /** - * Save out polydata to track format for use as input to lidar-optimize. - * - * @param polydata polydata to convert - * @param filename filename to write - * @param useOverlappingPoints if true, only points overlapping the reference model will be added - * to the track file. - */ - private void convertPolyDataToTrackFormat( - vtkPolyData polydata, String filename, boolean useOverlappingPoints) { - vtkPoints polyDataPoints = polydata.GetPoints(); - vtkPoints points = polyDataPoints; + private String tmpdir; - if (useOverlappingPoints) { - vtkPoints modelPoints = polyDataTruth.GetPoints(); - vtkUnstructuredGrid overlap = PolyDataUtil.intersectingPoints(modelPoints, polyDataPoints); - points = overlap.GetPoints(); + private static class DistanceContainer implements Comparable { + double closestDistance; + double normalDistance; + + public DistanceContainer(double closestDistance) { + super(); + this.closestDistance = closestDistance; + this.normalDistance = -1; + } + + public void setNormalDistance(double normalDistance) { + this.normalDistance = normalDistance; + } + + public double getClosestDistance() { + return closestDistance; + } + + public double getNormalDistance() { + return normalDistance; + } + + @Override + public int compareTo(DistanceContainer o) { + return Double.compare(closestDistance, o.closestDistance); + } } - try (PrintWriter pw = new PrintWriter(filename)) { - double[] p = new double[3]; - for (int i = 0; i < points.GetNumberOfPoints(); ++i) { - points.GetPoint(i, p); - pw.write( - String.format( - "2010-11-11T00:00:00.000 " - + p[0] - + " " - + p[1] - + " " - + p[2] - + " " - + p[0] - + " " - + p[1] - + " " - + p[2] - + "\n")); - } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - /** - * Run the C++ lidar-optimize code and store the derived transformation. - * - * @param computeOptimalTranslation true if translation should be computed - * @param computeOptimalRotation true if rotation should be computed - * @param maxNumberOfControlPoints maximum number of control points - * @param useOverlappingPoints if true, only points overlapping the reference model will be used - * @param transformationFile JSON file to write - */ - public void computeOptimalTransformationToTarget( - boolean computeOptimalTranslation, - boolean computeOptimalRotation, - int maxNumberOfControlPoints, - boolean useOverlappingPoints, - String transformationFile) { - File tmpDir = - new File( - String.format("%s%sCompareOBJ-%d", tmpdir, File.separator, System.currentTimeMillis())); - if (!tmpDir.exists()) tmpDir.mkdirs(); - - String trackFile = tmpDir + File.separator + "shapemodel-as-track.txt"; - convertPolyDataToTrackFormat(polyDataModel, trackFile, useOverlappingPoints); - - String tmpTransformationFile = tmpDir + File.separator + "transformationfile.txt"; - String transformationType = null; - if (computeOptimalTranslation && computeOptimalRotation) transformationType = ""; - else if (computeOptimalTranslation) transformationType = "--translation-only "; - else if (computeOptimalRotation) transformationType = "--rotation-only "; - - File lsk = ResourceUtils.writeResourceToFile("/resources/kernels/lsk/naif0012.tls"); - String command = - "lidar-optimize --max-number-control-points " - + maxNumberOfControlPoints - + " " - + transformationType - + "--load-from-file " - + lsk.getPath() - + " " - + referenceModelName - + " " - + trackFile - + " 0 0 " - + tmpTransformationFile; - - long startTime = System.currentTimeMillis(); - try { - ProcessUtils.runProgramAndWait(command, null, false); - } catch (IOException | InterruptedException e) { - logger.error(e.getLocalizedMessage(), e); - } - long duration = System.currentTimeMillis() - startTime; - logger.info(String.format("Execution time %.3f seconds", duration / 1e3)); - - // save a copy of the JSON transformation file - if (transformationFile != null) { - try (PrintWriter pw = new PrintWriter(transformationFile)) { - List lines = - FileUtils.readLines(new File(tmpTransformationFile), Charset.defaultCharset()); - for (String line : lines) pw.println(line); - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - transform = LidarTransformation.fromJSON(new File(tmpTransformationFile)); - } - - /** - * Transform the model polydata with the lidar transform. - * - * @param saveOptimalShape if true, write out transformed shape - * @param optimalShapeFile name of shape file to write - */ - private void transformPolyData(boolean saveOptimalShape, String optimalShapeFile) { - // Transform all points in inpolydata1 - long numPoints = polyDataModel.GetNumberOfPoints(); - vtkPoints points = polyDataModel.GetPoints(); - for (int i = 0; i < numPoints; ++i) { - Vector3D transformedPoint = transform.transformPoint(new Vector3D(points.GetPoint(i))); - points.SetPoint(i, transformedPoint.toArray()); - } - - if (saveOptimalShape) { - if (inputFormat.pointsOnly) { - FORMATS outputFormat = FORMATS.formatFromExtension(optimalShapeFile); - PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inputFormat, outputFormat); - pcfc.setPoints(polyDataModel.GetPoints()); - pcfc.write(optimalShapeFile, false); - } else { + public CompareOBJ(String modelName, String referenceModelName) { + this.referenceModelName = referenceModelName; try { - PolyDataUtil.saveShapeModelAsOBJ(polyDataModel, optimalShapeFile); + inputFormat = FORMATS.formatFromExtension(modelName); + if (inputFormat.pointsOnly) { + vtkPoints points = PointCloudFormatConverter.readPointCloud(modelName); + polyDataModel = new vtkPolyData(); + polyDataModel.SetPoints(points); + } else polyDataModel = PolyDataUtil.loadShapeModel(modelName); + polyDataTruth = PolyDataUtil.loadShapeModel(referenceModelName); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + System.exit(0); + } + + this.tmpdir = "."; + } + + public void setTmpdir(String tmpdir) { + this.tmpdir = tmpdir; + } + + /** + * Save out polydata to track format for use as input to lidar-optimize. + * + * @param polydata polydata to convert + * @param filename filename to write + * @param useOverlappingPoints if true, only points overlapping the reference model will be added + * to the track file. + */ + private void convertPolyDataToTrackFormat(vtkPolyData polydata, String filename, boolean useOverlappingPoints) { + vtkPoints polyDataPoints = polydata.GetPoints(); + vtkPoints points = polyDataPoints; + + if (useOverlappingPoints) { + vtkPoints modelPoints = polyDataTruth.GetPoints(); + vtkUnstructuredGrid overlap = PolyDataUtil.intersectingPoints(modelPoints, polyDataPoints); + points = overlap.GetPoints(); + } + + try (PrintWriter pw = new PrintWriter(filename)) { + double[] p = new double[3]; + for (int i = 0; i < points.GetNumberOfPoints(); ++i) { + points.GetPoint(i, p); + pw.write(String.format("2010-11-11T00:00:00.000 " + + p[0] + + " " + + p[1] + + " " + + p[2] + + " " + + p[0] + + " " + + p[1] + + " " + + p[2] + + "\n")); + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + /** + * Run the C++ lidar-optimize code and store the derived transformation. + * + * @param computeOptimalTranslation true if translation should be computed + * @param computeOptimalRotation true if rotation should be computed + * @param maxNumberOfControlPoints maximum number of control points + * @param useOverlappingPoints if true, only points overlapping the reference model will be used + * @param transformationFile JSON file to write + */ + public void computeOptimalTransformationToTarget( + boolean computeOptimalTranslation, + boolean computeOptimalRotation, + int maxNumberOfControlPoints, + boolean useOverlappingPoints, + String transformationFile) { + File tmpDir = new File(String.format("%s%sCompareOBJ-%d", tmpdir, File.separator, System.currentTimeMillis())); + if (!tmpDir.exists()) tmpDir.mkdirs(); + + String trackFile = tmpDir + File.separator + "shapemodel-as-track.txt"; + convertPolyDataToTrackFormat(polyDataModel, trackFile, useOverlappingPoints); + + String tmpTransformationFile = tmpDir + File.separator + "transformationfile.txt"; + String transformationType = null; + if (computeOptimalTranslation && computeOptimalRotation) transformationType = ""; + else if (computeOptimalTranslation) transformationType = "--translation-only "; + else if (computeOptimalRotation) transformationType = "--rotation-only "; + + File lsk = ResourceUtils.writeResourceToFile("/resources/kernels/lsk/naif0012.tls"); + String command = "lidar-optimize --max-number-control-points " + + maxNumberOfControlPoints + + " " + + transformationType + + "--load-from-file " + + lsk.getPath() + + " " + + referenceModelName + + " " + + trackFile + + " 0 0 " + + tmpTransformationFile; + + long startTime = System.currentTimeMillis(); + try { + ProcessUtils.runProgramAndWait(command, null, false); + } catch (IOException | InterruptedException e) { + logger.error(e.getLocalizedMessage(), e); + } + long duration = System.currentTimeMillis() - startTime; + logger.info(String.format("Execution time %.3f seconds", duration / 1e3)); + + // save a copy of the JSON transformation file + if (transformationFile != null) { + try (PrintWriter pw = new PrintWriter(transformationFile)) { + List lines = FileUtils.readLines(new File(tmpTransformationFile), Charset.defaultCharset()); + for (String line : lines) pw.println(line); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + transform = LidarTransformation.fromJSON(new File(tmpTransformationFile)); + } + + /** + * Transform the model polydata with the lidar transform. + * + * @param saveOptimalShape if true, write out transformed shape + * @param optimalShapeFile name of shape file to write + */ + private void transformPolyData(boolean saveOptimalShape, String optimalShapeFile) { + // Transform all points in inpolydata1 + long numPoints = polyDataModel.GetNumberOfPoints(); + vtkPoints points = polyDataModel.GetPoints(); + for (int i = 0; i < numPoints; ++i) { + Vector3D transformedPoint = transform.transformPoint(new Vector3D(points.GetPoint(i))); + points.SetPoint(i, transformedPoint.toArray()); + } + + if (saveOptimalShape) { + if (inputFormat.pointsOnly) { + FORMATS outputFormat = FORMATS.formatFromExtension(optimalShapeFile); + PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inputFormat, outputFormat); + pcfc.setPoints(polyDataModel.GetPoints()); + pcfc.write(optimalShapeFile, false); + } else { + try { + PolyDataUtil.saveShapeModelAsOBJ(polyDataModel, optimalShapeFile); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + } + } + + /** + * return distances between each pair of points projected onto a plane + * + * @param points List of points + * @param fitPlane plane to project points + * @return distances on the plane + */ + private List findDistances(List points, Plane fitPlane) { + List projectedPoints = new ArrayList<>(); + for (Vector3 point : points) { + try { + projectedPoints.add(fitPlane.project(point)); + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + List distances = new ArrayList<>(); + for (int i = 0; i < projectedPoints.size(); i++) { + Vector3 point0 = projectedPoints.get(i); + for (int j = i + 1; j < projectedPoints.size(); j++) { + Vector3 point1 = projectedPoints.get(j); + distances.add(point1.sub(point0).norm()); + } + } + return distances; + } + + /** + * + * + *
    + *
  1. Have the code randomly select npts region across the model and find a distinguishing + * feature (I was thinking of a local maximum or minimum in this region). Then find the + * corresponding best fit point (i.e. the local minima or maxima) in the truth model. + *
  2. Determine the horizontal distance in the plane of the model between each one of these + * distinguishing features. Repeat the same exercise across the truth, but first fit a plane + * over the region of the truth that more or less match the extent of the model. + *
  3. Subtract from the distance measured in the plane of the model between the npts regions + * from the distance between the same npts regions in the truth model, and normalize the + * result by the distance between the npts regions in the truth. Multiply this number by the + * typical extent of maplet which should be input as an option; the default should be 5 m. + *
  4. + *
+ * + * @param npts number of regions to select over the shape model + * @param radius size of maplet + */ + private void assessHorizontalAccuracy(int npts, double radius) throws SpiceException { + // find npts random facets in the shape model + long numVertices = polyDataModel.GetNumberOfPoints(); + List vertexIDs = new ArrayList<>(); + do { + int nextID = (int) (numVertices * Math.random()); + if (!vertexIDs.contains(nextID)) vertexIDs.add(nextID); + } while (vertexIDs.size() < npts); + + // now find the distance between each pair of points in the plane of the model + Plane fitPlaneModel = PolyDataUtil.fitPlaneToPolyData(polyDataModel); + + double[] pt = new double[3]; + List points = new ArrayList<>(); + for (int vertexID : vertexIDs) { + polyDataModel.GetPoint(vertexID, pt); + points.add(new Vector3(pt)); + } + + List distancesModel = findDistances(points, fitPlaneModel); + + // now find each of these points in the truth model + double resolution = .1; // meters per pixel + int offset = 5; // number of pixels to slide model in each direction to get best match with + // truth + + // 2D array of heights of the reference shape model above the reference plane + Plane fitPlaneReference = PolyDataUtil.fitPlaneToPolyData(polyDataTruth); + + XYGrid xyGridModel = new XYGrid(fitPlaneModel, resolution, radius, polyDataModel); + XYGrid xyGridReference = new XYGrid(fitPlaneReference, resolution, 2 * radius, polyDataTruth); + + int gridShift = (xyGridReference.getNx() - xyGridModel.getNx()) / 2; + + Vector adjustedPoints = new Vector<>(); + + for (Vector3 point : points) { + // both have odd number of points in each dimension, reference grid is 2x-1 model grid, + // centered on same point + xyGridModel.buildHeightGrid(point); + xyGridReference.buildHeightGrid(point); + double[][] heightModel = xyGridModel.getHeightGrid(); + double[][] heightReference = xyGridReference.getHeightGrid(); + + double minSum = Double.MAX_VALUE; + int bestX = 0; + int bestY = 0; + for (int yOffset = -offset; yOffset <= offset; yOffset++) { + for (int xOffset = -offset; xOffset <= offset; xOffset++) { + double sum = 0; + for (int j = 0; j < xyGridModel.getNy(); j++) { + int iyModel = j; + int iyReference = iyModel + yOffset + gridShift; + for (int i = 0; i < xyGridModel.getNy(); i++) { + int ixModel = i; + int ixReference = ixModel + xOffset + gridShift; + double modelHeight = heightModel[iyModel][ixModel]; + double refHeight = heightReference[iyReference][ixReference]; + if (!Double.isNaN(modelHeight) && !Double.isNaN(refHeight)) { + sum += Math.abs(modelHeight - refHeight); + // if (Math.abs(xOffset) < 2 && Math.abs(yOffset) < 2) + // { + // System.out.printf("%d %d %d %d %d %d %f %f\n", + // xOffset, yOffset, + // ixModel, iyModel, + // ixReference, iyReference, + // modelHeight, refHeight); + // } + } + } + } + if (sum < minSum) { + minSum = sum; + bestX = xOffset; + bestY = yOffset; + } + // System.out.printf("%d %d %6.2f\n", xOffset, yOffset, sum); + } + } // for (int yOffset = -offset; yOffset <= offset; yOffset++) + + // System.out.printf("%d %d %f\n", bestX, bestY, minSum); + + Vector3 adjustedPoint = xyGridReference.shift(fitPlaneReference.project(point), bestX, bestY); + adjustedPoints.add(adjustedPoint); + } + + // find the distance between each pair of points in the plane of the truth model + + List distancesTruth = findDistances(adjustedPoints, fitPlaneModel); + + double sumDiff = 0; + for (int i = 0; i < distancesModel.size(); i++) { + sumDiff += (Math.abs(distancesModel.get(i) - distancesTruth.get(i))) / distancesTruth.get(i); + } + sumDiff *= radius; + + System.out.printf( + "Sum normalized difference in horizontal distances (%d points used): %f meters\n", npts, sumDiff); + } + + /** + * Write out difference files. + * + * @param closestDiffFile argument to -savePlateDiff + * @param closestIndexFile argument to -savePlateIndex + * @param verticalDiffFile argument to -computeVerticalError + * @param limitClosestPoints argument to -limitClosestPoints + * @param radius argument to -fitPlaneRadius + */ + private void computeDifferences( + String closestDiffFile, + String closestIndexFile, + String verticalDiffFile, + double limitClosestPoints, + double radius) + throws IOException { + final boolean saveDiff = (closestDiffFile != null); + // only valid for shape models with facet information + final boolean saveIndex = !inputFormat.pointsOnly && (closestIndexFile != null); + final boolean computeVerticalError = (verticalDiffFile != null); + final boolean fitLocalPlane = (radius > 0); + + BufferedWriter outClosest = null; + if (saveDiff) { + FileWriter fstream = new FileWriter(closestDiffFile); + outClosest = new BufferedWriter(fstream); + } + BufferedWriter outClosestIndices = null; + if (saveIndex) { + FileWriter fstream = new FileWriter(closestIndexFile); + outClosestIndices = new BufferedWriter(fstream); + outClosestIndices.write("# plate index, closest reference plate index, distance\n"); + } + BufferedWriter outVertical = null; + if (computeVerticalError) { + FileWriter fstream = new FileWriter(verticalDiffFile); + outVertical = new BufferedWriter(fstream); + } + + // fit a plane to the entire shape model - no good for global shape models + Pair pair = PolyDataUtil.findLocalFrame(polyDataTruth); + Vector3D normal = pair.getKey().applyInverseTo(Vector3D.PLUS_K); + + SmallBodyModel smallBodyModel = null; + SmallBodyModel smallBodyTruth = new SmallBodyModel(polyDataTruth); + + long numPoints; + vtkIdList idList = new vtkIdList(); + vtkOctreePointLocator pointLocator = new vtkOctreePointLocator(); + if (inputFormat.pointsOnly) { + if (fitLocalPlane) { + pointLocator.FreeSearchStructure(); + vtkPolyData pointSet = new vtkPolyData(); + pointSet.SetPoints(polyDataModel.GetPoints()); + pointLocator.SetDataSet(pointSet); + pointLocator.BuildLocator(); + } + numPoints = polyDataModel.GetNumberOfPoints(); + } else { + smallBodyModel = new SmallBodyModel(polyDataModel); + numPoints = polyDataModel.GetNumberOfCells(); + } + + List distanceContainerVector = new ArrayList<>(); + int numPlatesActuallyUsed = 0; + + // loop through each cell in the model and find the closest point in the reference model + for (int i = 0; i < numPoints; ++i) { + + Vector3D p; + if (inputFormat.pointsOnly) { + + p = new Vector3D(polyDataModel.GetPoint(i)); + + if (fitLocalPlane) { + + // fit a plane to all point cloud points within radius of p + pointLocator.FindPointsWithinRadius(radius, p.toArray(), idList); + + if (idList.GetNumberOfIds() < 3) { + logger.error(String.format( + "point %d (%f %f %f): %d points within %f, using radial vector to find intersection", + i, p.getX(), p.getY(), p.getZ(), idList.GetNumberOfIds(), radius)); + normal = p.normalize(); + } else { + vtkPoints tmpPoints = new vtkPoints(); + for (int j = 0; j < idList.GetNumberOfIds(); j++) { + tmpPoints.InsertNextPoint(polyDataModel.GetPoint(idList.GetId(j))); + } + + PointCloudToPlane pctp = new PointCloudToPlane(tmpPoints); + normal = new Vector3D(pctp.getGMU().getPlaneNormal()); + } + } + + } else { + + TriangularFacet facet = PolyDataUtil.getFacet(polyDataModel, i); + p = MathConversions.toVector3D(facet.getCenter()); + if (fitLocalPlane) { + normal = new Vector3D(smallBodyModel.getNormalAtPoint(p.toArray(), radius)); + } else { + normal = MathConversions.toVector3D(facet.getNormal()); + } + } + + Optional normalPoint = findIntersectPointInNormalDirection(p, normal, smallBodyTruth); + DistanceContainer dc = null; + + // Skip this plate in the error calculation if there is no intersection + if (normalPoint.isPresent()) { + Vector3D closestPoint = new Vector3D(smallBodyTruth.findClosestPoint(p.toArray())); + double closestDistance = p.distance(closestPoint); + dc = new DistanceContainer(closestDistance); + distanceContainerVector.add(dc); + + if (saveDiff || saveIndex) { + // Determining if p from model 1 is inside or outside of ref surface + // model 2. Negative distance means p is inside the ref model which + // corresponds to a positive dot product + Vector3D pdiff = closestPoint.subtract(p); + if (pdiff.dotProduct(p) > 0) closestDistance *= -1.0; + + if (saveDiff) outClosest.write(closestDistance + "\n"); + + if (saveIndex) { + long closestFacet = smallBodyTruth.findClosestCell(p.toArray()); + outClosestIndices.write(String.format("%d, %d, %f\n", i, closestFacet, closestDistance)); + } + } + + } else { + if (saveDiff) outClosest.write("no-intersection\n"); + } + + if (computeVerticalError) { + if (normalPoint.isPresent()) { + double normalDistance = p.distance(normalPoint.get()); + dc.setNormalDistance(normalDistance); + + Vector3D pdiff = normalPoint.get().subtract(p); + if (pdiff.dotProduct(p) > 0) normalDistance *= -1.0; + + outVertical.write(normalDistance + "\n"); + } else { + outVertical.write("no-intersection\n"); + } + } + + if (normalPoint.isPresent()) ++numPlatesActuallyUsed; + } // for (int i = 0; i < numPlates; ++i) + + if (computeVerticalError) outVertical.close(); + + if (saveDiff) outClosest.close(); + + if (saveIndex) outClosestIndices.close(); + + numPoints = (int) (limitClosestPoints * distanceContainerVector.size() + 0.5); + double closestDistanceError = 0.0; + double closestDistance2Error = 0.0; + double normalDistanceError = 0.0; + double normalDistance2Error = 0.0; + double minDist = 0.0; + double maxDist = 0.0; + if (!distanceContainerVector.isEmpty()) { + Collections.sort(distanceContainerVector); + closestDistanceError = 0; + closestDistance2Error = 0; + for (int i = 0; i < numPoints; i++) { + double distance = distanceContainerVector.get(i).getClosestDistance(); + closestDistanceError += distance; + closestDistance2Error += distance * distance; + } + closestDistanceError /= numPoints; + closestDistance2Error /= numPoints; + if (computeVerticalError) { + for (int i = 0; i < numPoints; i++) { + double distance = distanceContainerVector.get(i).getNormalDistance(); + normalDistanceError += distance; + normalDistance2Error += distance * distance; + } + normalDistanceError /= numPoints; + normalDistance2Error /= numPoints; + } + + minDist = distanceContainerVector.get(0).getClosestDistance(); + maxDist = distanceContainerVector + .get(distanceContainerVector.size() - 1) + .getClosestDistance(); + } + + Vector3D translation = transform.getTranslation(); + String tmpString = + String.format("%16.8e,%16.8e,%16.8e", translation.getX(), translation.getY(), translation.getZ()); + System.out.println("Translation: " + tmpString.replaceAll("\\s+", "")); + + Rotation rotation = transform.getRotation(); + Rotation inverse = rotation.composeInverse(Rotation.IDENTITY, RotationConvention.FRAME_TRANSFORM); + Quaternion q = new Quaternion(inverse.getQ0(), inverse.getQ1(), inverse.getQ2(), inverse.getQ3()) + .getPositivePolarForm(); + + tmpString = String.format("%16.8e,%16.8e,%16.8e,%16.8e", q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3()); + System.out.println("Rotation quaternion: " + tmpString.replaceAll("\\s+", "")); + + Vector3D axis = inverse.getAxis(RotationConvention.FRAME_TRANSFORM); + tmpString = String.format( + "%16.8e,%16.8e,%16.8e,%16.8e", + Math.toDegrees(inverse.getAngle()), axis.getX(), axis.getY(), axis.getZ()); + System.out.println("Rotation angle (degrees) and axis: " + tmpString.replaceAll("\\s+", "")); + + Vector3D centerOfRotation = transform.getCenterOfRotation(); + tmpString = String.format( + "%16.8e,%16.8e,%16.8e", centerOfRotation.getX(), centerOfRotation.getY(), centerOfRotation.getZ()); + System.out.println("Center of rotation: " + tmpString.replaceAll("\\s+", "")); + + double[][] rotMatrix = inverse.getMatrix(); + double[] translationArray = translation.toArray(); + System.out.println("4x4 Transformation matrix:"); + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 3; i++) { + // SPICE defines its matrices with the row as the first index and Apache Commons Rotation + // uses the column as the first index. We want to write out each row of the matrix. + System.out.printf("%16.8e ", rotMatrix[i][j]); + } + System.out.printf("%16.8e\n", translationArray[j]); + } + System.out.printf("%16.8e %16.8e %16.8e %16.8e\n", 0., 0., 0., 1.); + + System.out.printf( + "Using %d of %d points (excluding %.1f%% largest distances)\n", + numPoints, + distanceContainerVector.size(), + 100 * (1 - ((double) numPoints) / distanceContainerVector.size())); + System.out.println("Min Distance: " + minDist); + System.out.println("Max Distance: " + maxDist); + System.out.println("Mean Distance: " + closestDistanceError); + System.out.println("Mean Square Distance: " + closestDistance2Error); + System.out.println("Root Mean Square Distance: " + Math.sqrt(closestDistance2Error)); + + if (computeVerticalError) { + System.out.println("Mean Vertical Distance: " + normalDistanceError); + System.out.println("Mean Square Vertical Distance: " + normalDistance2Error); + System.out.println("Root Mean Square Vertical Distance: " + Math.sqrt(normalDistance2Error)); + + if (!fitLocalPlane) { + + // plane containing the origin + Vector3D unitNormal = normal.normalize(); + + org.apache.commons.math3.geometry.euclidean.threed.Plane p = + new org.apache.commons.math3.geometry.euclidean.threed.Plane(Vector3D.ZERO, unitNormal, 1e-6); + Vector3D parallelVector = p.toSpace(p.toSubSpace(translation)); + + System.out.println("Direction perpendicular to plane: " + + unitNormal.getX() + + " " + + unitNormal.getY() + + " " + + unitNormal.getZ()); + System.out.println( + "Magnitude of projection perpendicular to plane: " + translation.dotProduct(unitNormal)); + System.out.println("Projection vector of translation parallel to plane: " + + parallelVector.getX() + + " " + + parallelVector.getY() + + " " + + parallelVector.getZ()); + System.out.println( + "Magnitude of projection vector of translation parallel to plane: " + parallelVector.getNorm()); + + /*- SPICE + + // plane containing the origin + Vector3 normalVector = VectorUtils.toVector3(normal).hat(); + Vector3 translationVector = VectorUtils.toVector3(translation); + try { + Plane p = new Plane(VectorUtils.toVector3(normal), new Vector3()); + Vector3 parallelVector = p.project(translationVector); + normalVector = normalVector.scale(translationVector.dot(normalVector)); + + System.out.println("Direction perpendicular to plane: " + normalVector.getElt(0) + " " + + normalVector.getElt(1) + " " + normalVector.getElt(2)); + System.out + .println("Magnitude of projection perpendicular to plane: " + normalVector.norm()); + System.out.println( + "Projection vector of translation parallel to plane: " + parallelVector.getElt(0) + + " " + parallelVector.getElt(1) + " " + parallelVector.getElt(2)); + System.out.println("Magnitude of projection vector of translation parallel to plane: " + + parallelVector.norm()); + } catch (SpiceException e) { + SimpleLogger.getInstance().warn(e.getLocalizedMessage()); + } + + */ + + } + + System.out.println(numPlatesActuallyUsed + + " plates used in error calculation out of " + + polyDataModel.GetNumberOfCells() + + " total in the shape model"); + } + } + + /** + * Shoot 2 rays: one from "on top" and a second from "below". Ideally, both should be identical. + * But if not, then return the closest one. + * + * @param in + * @param normal + * @param smallBodyModel + * @return + */ + private static Optional findIntersectPointInNormalDirection( + Vector3D in, Vector3D normal, SmallBodyModel smallBodyModel) { + + double size = Math.max(smallBodyModel.getBoundingBoxDiagonalLength(), in.getNorm()); + + // First do the intersection from "below" + Vector3D startBottom = in.subtract(normal.scalarMultiply(size)); + double[] out1 = new double[3]; + long cellId1 = smallBodyModel.computeRayIntersection(startBottom.toArray(), normal.toArray(), out1); + + // Then do the intersection from on "top" + Vector3D startTop = in.add(normal.scalarMultiply(size)); + double[] out2 = new double[3]; + long cellId2 = smallBodyModel.computeRayIntersection( + startTop.toArray(), normal.negate().toArray(), out2); + + if (cellId1 >= 0 && cellId2 >= 0) { + Vector3D out1V = new Vector3D(out1); + Vector3D out2V = new Vector3D(out2); + + Vector3D inSubOut1 = in.subtract(out1V); + Vector3D inSubOut2 = in.subtract(out2V); + // If both intersected, take the closest + if (inSubOut1.dotProduct(inSubOut1) < inSubOut2.dotProduct(inSubOut2)) return Optional.of(out1V); + else return Optional.of(out2V); + } + if (cellId1 >= 0) return Optional.of(new Vector3D(out1)); + + if (cellId2 >= 0) return Optional.of(new Vector3D(out2)); + + return Optional.empty(); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("logFile") + .hasArg() + .argName("path") + .desc("If present, save screen output to .") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + + sb = new StringBuilder(); + sb.append("If specified, the program first computes the optimal rotation of "); + sb.append("-model so that it best matches -reference prior "); + sb.append("to computing the error between them. Prior to computing the errors, "); + sb.append("-model is transformed to -reference using this optimal "); + sb.append("rotation. This results in an error unbiased by a possible rotation between "); + sb.append("the two models."); + options.addOption( + Option.builder("computeOptimalRotation").desc(sb.toString()).build()); + + sb = new StringBuilder(); + sb.append("If specified, the program first computes the optimal translation of "); + sb.append("-model so that it best matches -reference prior to "); + sb.append("computing the error between them. Prior to computing the errors, -model "); + sb.append("is transformed to the -reference using this optimal translation. "); + sb.append("This results in an error unbiased by a possible translation offset between "); + sb.append("the two models."); + options.addOption( + Option.builder("computeOptimalTranslation").desc(sb.toString()).build()); + + sb = new StringBuilder(); + sb.append("If specified, the program first computes the optimal translation and "); + sb.append("rotation of -model so that it best matches -reference "); + sb.append("prior to computing the error between them. Prior to computing the errors, "); + sb.append("-model is transformed to -reference using this optimal "); + sb.append("translation and rotation. This results in an error unbiased by a possible "); + sb.append("translation offset or rotation between the two models."); + options.addOption(Option.builder("computeOptimalRotationAndTranslation") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("If specified, the program computes the error in the vertical direction "); + sb.append("(by fitting a plane to -model) and saves it to . This option "); + sb.append("only produces meaningful results for digital terrain models to which a plane "); + sb.append("can be fit."); + options.addOption(Option.builder("computeVerticalError") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Use the normal to a plane fit to the model when computing distances to the "); + sb.append("reference. If this option is absent, a plane is fit to the entire model. If "); + sb.append("present, only points within a distance of radius will be used to construct the "); + sb.append("plane. Recommended value is ~5% of the body radius for a global model. Units "); + sb.append("are units of the shape model."); + options.addOption( + Option.builder("fitPlaneRadius").hasArg().desc(sb.toString()).build()); + + sb = new StringBuilder(); + sb.append("Limit the distances (described in -savePlateDiff) used in calculating the mean "); + sb.append("distance and RMS distance to the closest fraction of all distances."); + options.addOption(Option.builder("limitClosestPoints") + .hasArg() + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Max number of control points to use in optimization. Default is 2000."); + options.addOption(Option.builder("maxNumberControlPoints") + .hasArg() + .desc(sb.toString()) + .build()); + + options.addOption(Option.builder("model") + .required() + .hasArg() + .argName("path") + .desc("Required. Point cloud/shape file to compare to reference shape. Valid formats are " + + "anything that can be read by the PointCloudFormatConverter.") + .build()); + options.addOption(Option.builder("reference") + .required() + .hasArg() + .argName("path") + .desc("Required. Reference shape file. Valid formats are FITS, ICQ, OBJ, PLT, PLY, or VTK.") + .build()); + + sb = new StringBuilder(); + sb.append("Save the rotated and/or translated -model to . "); + sb.append("This option requires one of -computeOptimalRotation, -computeOptimalTranslation "); + sb.append("or -computeOptimalRotationAndTranslation to be specified."); + options.addOption(Option.builder("saveOptimalShape") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Save to a file specified by the distances (in same units as the shape "); + sb.append("models) between each plate center of -model to the closest point in "); + sb.append("-reference. The number of lines in the file equals the number of plates in "); + sb.append("the first shape model with each line containing the distance of that plate "); + sb.append("to the closest point in the second shape model."); + options.addOption(Option.builder("savePlateDiff") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Save to a file specified by the index of the closest plate in "); + sb.append("second (reference) shape model to the center of each plate in the first model. "); + sb.append("The format of each line is "); + sb.append("\n\tplate index, closest reference plate index, distance\n"); + sb.append("Only valid when -model is a shape model with facet information."); + options.addOption(Option.builder("savePlateIndex") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Save output of lidar-optimize to (JSON format file)"); + options.addOption(Option.builder("saveTransformationFile") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Directory to store temporary files. It will be created if it does not exist. "); + sb.append("Default is the current working directory."); + options.addOption(Option.builder("tmpDir") + .hasArg() + .argName("path") + .desc(sb.toString()) + .build()); + + sb = new StringBuilder(); + sb.append("Use all points in -model when attempting to fit to "); + sb.append("-reference. The default behavior is to use only points which overlap the "); + sb.append("reference model."); + options.addOption(Option.builder("useAllPoints").desc(sb.toString()).build()); + return options; + } + + public static void main(String[] args) { + + TerrasaurTool defaultOBJ = new CompareOBJ(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + double limitClosestPoints = + cl.hasOption("limitClosestPoints") ? Double.parseDouble(cl.getOptionValue("limitClosestPoints")) : 1.0; + limitClosestPoints = Math.max(0, Math.min(1, limitClosestPoints)); + + boolean saveOptimalShape = cl.hasOption("saveOptimalShape"); + boolean computeOptimalTranslation = + cl.hasOption("computeOptimalRotationAndTranslation") || cl.hasOption("computeOptimalTranslation"); + boolean computeOptimalRotation = + cl.hasOption("computeOptimalRotationAndTranslation") || cl.hasOption("computeOptimalRotation"); + final boolean computeHorizontalError = false; + boolean useOverlappingPoints = !cl.hasOption("useAllPoints"); + double planeRadius = + cl.hasOption("fitPlaneRadius") ? Double.parseDouble(cl.getOptionValue("fitPlaneRadius")) : 0; + int maxNumberOfControlPoints = cl.hasOption("maxNumberControlPoints") + ? Integer.parseInt(cl.getOptionValue("maxNumberControlPoints")) + : 2000; + int npoints = -1; + double size = 0; + String closestDiffFile = cl.hasOption("savePlateDiff") ? cl.getOptionValue("savePlateDiff") : null; + String closestIndexFile = cl.hasOption("savePlateIndex") ? cl.getOptionValue("savePlateIndex") : null; + String optimalShapeFile = saveOptimalShape ? cl.getOptionValue("saveOptimalShape") : null; + String verticalDiffFile = + cl.hasOption("computeVerticalError") ? cl.getOptionValue("computeVerticalError") : null; + String transformationFile = + cl.hasOption("saveTransformationFile") ? cl.getOptionValue("saveTransformationFile") : null; + String tmpdir = cl.hasOption("tmpDir") ? cl.getOptionValue("tmpDir") : "."; + + String infile1 = cl.getOptionValue("model"); + String infile2 = cl.getOptionValue("reference"); + + NativeLibraryLoader.loadSpiceLibraries(); + NativeLibraryLoader.loadVtkLibraries(); + + CompareOBJ compareOBJ = new CompareOBJ(infile1, infile2); + compareOBJ.setTmpdir(tmpdir); + + if (computeOptimalTranslation || computeOptimalRotation) { + compareOBJ.computeOptimalTransformationToTarget( + computeOptimalTranslation, + computeOptimalRotation, + maxNumberOfControlPoints, + useOverlappingPoints, + transformationFile); + + compareOBJ.transformPolyData(saveOptimalShape, optimalShapeFile); + } + + // note: this will never be called + if (computeHorizontalError) { + try { + compareOBJ.assessHorizontalAccuracy(npoints, size); + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + try { + compareOBJ.computeDifferences( + closestDiffFile, closestIndexFile, verticalDiffFile, limitClosestPoints, planeRadius); } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); + logger.error(e.getLocalizedMessage(), e); } - } } - } - - /** - * return distances between each pair of points projected onto a plane - * - * @param points List of points - * @param fitPlane plane to project points - * @return distances on the plane - */ - private List findDistances(List points, Plane fitPlane) { - List projectedPoints = new ArrayList<>(); - for (Vector3 point : points) { - try { - projectedPoints.add(fitPlane.project(point)); - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - List distances = new ArrayList<>(); - for (int i = 0; i < projectedPoints.size(); i++) { - Vector3 point0 = projectedPoints.get(i); - for (int j = i + 1; j < projectedPoints.size(); j++) { - Vector3 point1 = projectedPoints.get(j); - distances.add(point1.sub(point0).norm()); - } - } - return distances; - } - - /** - * - * - *
    - *
  1. Have the code randomly select npts region across the model and find a distinguishing - * feature (I was thinking of a local maximum or minimum in this region). Then find the - * corresponding best fit point (i.e. the local minima or maxima) in the truth model. - *
  2. Determine the horizontal distance in the plane of the model between each one of these - * distinguishing features. Repeat the same exercise across the truth, but first fit a plane - * over the region of the truth that more or less match the extent of the model. - *
  3. Subtract from the distance measured in the plane of the model between the npts regions - * from the distance between the same npts regions in the truth model, and normalize the - * result by the distance between the npts regions in the truth. Multiply this number by the - * typical extent of maplet which should be input as an option; the default should be 5 m. - *
  4. - *
- * - * @param npts number of regions to select over the shape model - * @param radius size of maplet - */ - private void assessHorizontalAccuracy(int npts, double radius) throws SpiceException { - // find npts random facets in the shape model - long numVertices = polyDataModel.GetNumberOfPoints(); - List vertexIDs = new ArrayList<>(); - do { - int nextID = (int) (numVertices * Math.random()); - if (!vertexIDs.contains(nextID)) vertexIDs.add(nextID); - } while (vertexIDs.size() < npts); - - // now find the distance between each pair of points in the plane of the model - Plane fitPlaneModel = PolyDataUtil.fitPlaneToPolyData(polyDataModel); - - double[] pt = new double[3]; - List points = new ArrayList<>(); - for (int vertexID : vertexIDs) { - polyDataModel.GetPoint(vertexID, pt); - points.add(new Vector3(pt)); - } - - List distancesModel = findDistances(points, fitPlaneModel); - - // now find each of these points in the truth model - double resolution = .1; // meters per pixel - int offset = 5; // number of pixels to slide model in each direction to get best match with - // truth - - // 2D array of heights of the reference shape model above the reference plane - Plane fitPlaneReference = PolyDataUtil.fitPlaneToPolyData(polyDataTruth); - - XYGrid xyGridModel = new XYGrid(fitPlaneModel, resolution, radius, polyDataModel); - XYGrid xyGridReference = new XYGrid(fitPlaneReference, resolution, 2 * radius, polyDataTruth); - - int gridShift = (xyGridReference.getNx() - xyGridModel.getNx()) / 2; - - Vector adjustedPoints = new Vector<>(); - - for (Vector3 point : points) { - // both have odd number of points in each dimension, reference grid is 2x-1 model grid, - // centered on same point - xyGridModel.buildHeightGrid(point); - xyGridReference.buildHeightGrid(point); - double[][] heightModel = xyGridModel.getHeightGrid(); - double[][] heightReference = xyGridReference.getHeightGrid(); - - double minSum = Double.MAX_VALUE; - int bestX = 0; - int bestY = 0; - for (int yOffset = -offset; yOffset <= offset; yOffset++) { - for (int xOffset = -offset; xOffset <= offset; xOffset++) { - double sum = 0; - for (int j = 0; j < xyGridModel.getNy(); j++) { - int iyModel = j; - int iyReference = iyModel + yOffset + gridShift; - for (int i = 0; i < xyGridModel.getNy(); i++) { - int ixModel = i; - int ixReference = ixModel + xOffset + gridShift; - double modelHeight = heightModel[iyModel][ixModel]; - double refHeight = heightReference[iyReference][ixReference]; - if (!Double.isNaN(modelHeight) && !Double.isNaN(refHeight)) { - sum += Math.abs(modelHeight - refHeight); - // if (Math.abs(xOffset) < 2 && Math.abs(yOffset) < 2) - // { - // System.out.printf("%d %d %d %d %d %d %f %f\n", - // xOffset, yOffset, - // ixModel, iyModel, - // ixReference, iyReference, - // modelHeight, refHeight); - // } - } - } - } - if (sum < minSum) { - minSum = sum; - bestX = xOffset; - bestY = yOffset; - } - // System.out.printf("%d %d %6.2f\n", xOffset, yOffset, sum); - } - } // for (int yOffset = -offset; yOffset <= offset; yOffset++) - - // System.out.printf("%d %d %f\n", bestX, bestY, minSum); - - Vector3 adjustedPoint = xyGridReference.shift(fitPlaneReference.project(point), bestX, bestY); - adjustedPoints.add(adjustedPoint); - } - - // find the distance between each pair of points in the plane of the truth model - - List distancesTruth = findDistances(adjustedPoints, fitPlaneModel); - - double sumDiff = 0; - for (int i = 0; i < distancesModel.size(); i++) { - sumDiff += (Math.abs(distancesModel.get(i) - distancesTruth.get(i))) / distancesTruth.get(i); - } - sumDiff *= radius; - - System.out.printf( - "Sum normalized difference in horizontal distances (%d points used): %f meters\n", - npts, sumDiff); - } - - /** - * Write out difference files. - * - * @param closestDiffFile argument to -savePlateDiff - * @param closestIndexFile argument to -savePlateIndex - * @param verticalDiffFile argument to -computeVerticalError - * @param limitClosestPoints argument to -limitClosestPoints - * @param radius argument to -fitPlaneRadius - */ - private void computeDifferences( - String closestDiffFile, - String closestIndexFile, - String verticalDiffFile, - double limitClosestPoints, - double radius) - throws IOException { - final boolean saveDiff = (closestDiffFile != null); - // only valid for shape models with facet information - final boolean saveIndex = !inputFormat.pointsOnly && (closestIndexFile != null); - final boolean computeVerticalError = (verticalDiffFile != null); - final boolean fitLocalPlane = (radius > 0); - - BufferedWriter outClosest = null; - if (saveDiff) { - FileWriter fstream = new FileWriter(closestDiffFile); - outClosest = new BufferedWriter(fstream); - } - BufferedWriter outClosestIndices = null; - if (saveIndex) { - FileWriter fstream = new FileWriter(closestIndexFile); - outClosestIndices = new BufferedWriter(fstream); - outClosestIndices.write("# plate index, closest reference plate index, distance\n"); - } - BufferedWriter outVertical = null; - if (computeVerticalError) { - FileWriter fstream = new FileWriter(verticalDiffFile); - outVertical = new BufferedWriter(fstream); - } - - // fit a plane to the entire shape model - no good for global shape models - Pair pair = PolyDataUtil.findLocalFrame(polyDataTruth); - Vector3D normal = pair.getKey().applyInverseTo(Vector3D.PLUS_K); - - SmallBodyModel smallBodyModel = null; - SmallBodyModel smallBodyTruth = new SmallBodyModel(polyDataTruth); - - long numPoints; - vtkIdList idList = new vtkIdList(); - vtkOctreePointLocator pointLocator = new vtkOctreePointLocator(); - if (inputFormat.pointsOnly) { - if (fitLocalPlane) { - pointLocator.FreeSearchStructure(); - vtkPolyData pointSet = new vtkPolyData(); - pointSet.SetPoints(polyDataModel.GetPoints()); - pointLocator.SetDataSet(pointSet); - pointLocator.BuildLocator(); - } - numPoints = polyDataModel.GetNumberOfPoints(); - } else { - smallBodyModel = new SmallBodyModel(polyDataModel); - numPoints = polyDataModel.GetNumberOfCells(); - } - - List distanceContainerVector = new ArrayList<>(); - int numPlatesActuallyUsed = 0; - - // loop through each cell in the model and find the closest point in the reference model - for (int i = 0; i < numPoints; ++i) { - - Vector3D p; - if (inputFormat.pointsOnly) { - - p = new Vector3D(polyDataModel.GetPoint(i)); - - if (fitLocalPlane) { - - // fit a plane to all point cloud points within radius of p - pointLocator.FindPointsWithinRadius(radius, p.toArray(), idList); - - if (idList.GetNumberOfIds() < 3) { - logger.error( - String.format( - "point %d (%f %f %f): %d points within %f, using radial vector to find intersection", - i, p.getX(), p.getY(), p.getZ(), idList.GetNumberOfIds(), radius)); - normal = p.normalize(); - } else { - vtkPoints tmpPoints = new vtkPoints(); - for (int j = 0; j < idList.GetNumberOfIds(); j++) { - tmpPoints.InsertNextPoint(polyDataModel.GetPoint(idList.GetId(j))); - } - - PointCloudToPlane pctp = new PointCloudToPlane(tmpPoints); - normal = new Vector3D(pctp.getGMU().getPlaneNormal()); - } - } - - } else { - - TriangularFacet facet = PolyDataUtil.getFacet(polyDataModel, i); - p = MathConversions.toVector3D(facet.getCenter()); - if (fitLocalPlane) { - normal = new Vector3D(smallBodyModel.getNormalAtPoint(p.toArray(), radius)); - } else { - normal = MathConversions.toVector3D(facet.getNormal()); - } - } - - Optional normalPoint = - findIntersectPointInNormalDirection(p, normal, smallBodyTruth); - DistanceContainer dc = null; - - // Skip this plate in the error calculation if there is no intersection - if (normalPoint.isPresent()) { - Vector3D closestPoint = new Vector3D(smallBodyTruth.findClosestPoint(p.toArray())); - double closestDistance = p.distance(closestPoint); - dc = new DistanceContainer(closestDistance); - distanceContainerVector.add(dc); - - if (saveDiff || saveIndex) { - // Determining if p from model 1 is inside or outside of ref surface - // model 2. Negative distance means p is inside the ref model which - // corresponds to a positive dot product - Vector3D pdiff = closestPoint.subtract(p); - if (pdiff.dotProduct(p) > 0) closestDistance *= -1.0; - - if (saveDiff) outClosest.write(closestDistance + "\n"); - - if (saveIndex) { - long closestFacet = smallBodyTruth.findClosestCell(p.toArray()); - outClosestIndices.write( - String.format("%d, %d, %f\n", i, closestFacet, closestDistance)); - } - } - - } else { - if (saveDiff) outClosest.write("no-intersection\n"); - } - - if (computeVerticalError) { - if (normalPoint.isPresent()) { - double normalDistance = p.distance(normalPoint.get()); - dc.setNormalDistance(normalDistance); - - Vector3D pdiff = normalPoint.get().subtract(p); - if (pdiff.dotProduct(p) > 0) normalDistance *= -1.0; - - outVertical.write(normalDistance + "\n"); - } else { - outVertical.write("no-intersection\n"); - } - } - - if (normalPoint.isPresent()) ++numPlatesActuallyUsed; - } // for (int i = 0; i < numPlates; ++i) - - if (computeVerticalError) outVertical.close(); - - if (saveDiff) outClosest.close(); - - if (saveIndex) outClosestIndices.close(); - - numPoints = (int) (limitClosestPoints * distanceContainerVector.size() + 0.5); - double closestDistanceError = 0.0; - double closestDistance2Error = 0.0; - double normalDistanceError = 0.0; - double normalDistance2Error = 0.0; - double minDist = 0.0; - double maxDist = 0.0; - if (!distanceContainerVector.isEmpty()) { - Collections.sort(distanceContainerVector); - closestDistanceError = 0; - closestDistance2Error = 0; - for (int i = 0; i < numPoints; i++) { - double distance = distanceContainerVector.get(i).getClosestDistance(); - closestDistanceError += distance; - closestDistance2Error += distance * distance; - } - closestDistanceError /= numPoints; - closestDistance2Error /= numPoints; - if (computeVerticalError) { - for (int i = 0; i < numPoints; i++) { - double distance = distanceContainerVector.get(i).getNormalDistance(); - normalDistanceError += distance; - normalDistance2Error += distance * distance; - } - normalDistanceError /= numPoints; - normalDistance2Error /= numPoints; - } - - minDist = distanceContainerVector.get(0).getClosestDistance(); - maxDist = - distanceContainerVector.get(distanceContainerVector.size() - 1).getClosestDistance(); - } - - Vector3D translation = transform.getTranslation(); - String tmpString = - String.format( - "%16.8e,%16.8e,%16.8e", translation.getX(), translation.getY(), translation.getZ()); - System.out.println("Translation: " + tmpString.replaceAll("\\s+", "")); - - Rotation rotation = transform.getRotation(); - Rotation inverse = - rotation.composeInverse(Rotation.IDENTITY, RotationConvention.FRAME_TRANSFORM); - Quaternion q = - new Quaternion(inverse.getQ0(), inverse.getQ1(), inverse.getQ2(), inverse.getQ3()) - .getPositivePolarForm(); - - tmpString = - String.format("%16.8e,%16.8e,%16.8e,%16.8e", q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3()); - System.out.println("Rotation quaternion: " + tmpString.replaceAll("\\s+", "")); - - Vector3D axis = inverse.getAxis(RotationConvention.FRAME_TRANSFORM); - tmpString = - String.format( - "%16.8e,%16.8e,%16.8e,%16.8e", - Math.toDegrees(inverse.getAngle()), axis.getX(), axis.getY(), axis.getZ()); - System.out.println("Rotation angle (degrees) and axis: " + tmpString.replaceAll("\\s+", "")); - - Vector3D centerOfRotation = transform.getCenterOfRotation(); - tmpString = - String.format( - "%16.8e,%16.8e,%16.8e", - centerOfRotation.getX(), centerOfRotation.getY(), centerOfRotation.getZ()); - System.out.println("Center of rotation: " + tmpString.replaceAll("\\s+", "")); - - double[][] rotMatrix = inverse.getMatrix(); - double[] translationArray = translation.toArray(); - System.out.println("4x4 Transformation matrix:"); - for (int j = 0; j < 3; j++) { - for (int i = 0; i < 3; i++) { - // SPICE defines its matrices with the row as the first index and Apache Commons Rotation - // uses the column as the first index. We want to write out each row of the matrix. - System.out.printf("%16.8e ", rotMatrix[i][j]); - } - System.out.printf("%16.8e\n", translationArray[j]); - } - System.out.printf("%16.8e %16.8e %16.8e %16.8e\n", 0., 0., 0., 1.); - - System.out.printf( - "Using %d of %d points (excluding %.1f%% largest distances)\n", - numPoints, - distanceContainerVector.size(), - 100 * (1 - ((double) numPoints) / distanceContainerVector.size())); - System.out.println("Min Distance: " + minDist); - System.out.println("Max Distance: " + maxDist); - System.out.println("Mean Distance: " + closestDistanceError); - System.out.println("Mean Square Distance: " + closestDistance2Error); - System.out.println("Root Mean Square Distance: " + Math.sqrt(closestDistance2Error)); - - if (computeVerticalError) { - System.out.println("Mean Vertical Distance: " + normalDistanceError); - System.out.println("Mean Square Vertical Distance: " + normalDistance2Error); - System.out.println("Root Mean Square Vertical Distance: " + Math.sqrt(normalDistance2Error)); - - if (!fitLocalPlane) { - - // plane containing the origin - Vector3D unitNormal = normal.normalize(); - - org.apache.commons.math3.geometry.euclidean.threed.Plane p = - new org.apache.commons.math3.geometry.euclidean.threed.Plane( - Vector3D.ZERO, unitNormal, 1e-6); - Vector3D parallelVector = p.toSpace(p.toSubSpace(translation)); - - System.out.println( - "Direction perpendicular to plane: " - + unitNormal.getX() - + " " - + unitNormal.getY() - + " " - + unitNormal.getZ()); - System.out.println( - "Magnitude of projection perpendicular to plane: " - + translation.dotProduct(unitNormal)); - System.out.println( - "Projection vector of translation parallel to plane: " - + parallelVector.getX() - + " " - + parallelVector.getY() - + " " - + parallelVector.getZ()); - System.out.println( - "Magnitude of projection vector of translation parallel to plane: " - + parallelVector.getNorm()); - - /*- SPICE - - // plane containing the origin - Vector3 normalVector = VectorUtils.toVector3(normal).hat(); - Vector3 translationVector = VectorUtils.toVector3(translation); - try { - Plane p = new Plane(VectorUtils.toVector3(normal), new Vector3()); - Vector3 parallelVector = p.project(translationVector); - normalVector = normalVector.scale(translationVector.dot(normalVector)); - - System.out.println("Direction perpendicular to plane: " + normalVector.getElt(0) + " " - + normalVector.getElt(1) + " " + normalVector.getElt(2)); - System.out - .println("Magnitude of projection perpendicular to plane: " + normalVector.norm()); - System.out.println( - "Projection vector of translation parallel to plane: " + parallelVector.getElt(0) - + " " + parallelVector.getElt(1) + " " + parallelVector.getElt(2)); - System.out.println("Magnitude of projection vector of translation parallel to plane: " - + parallelVector.norm()); - } catch (SpiceException e) { - SimpleLogger.getInstance().warn(e.getLocalizedMessage()); - } - - */ - - } - - System.out.println( - numPlatesActuallyUsed - + " plates used in error calculation out of " - + polyDataModel.GetNumberOfCells() - + " total in the shape model"); - } - } - - /** - * Shoot 2 rays: one from "on top" and a second from "below". Ideally, both should be identical. - * But if not, then return the closest one. - * - * @param in - * @param normal - * @param smallBodyModel - * @return - */ - private static Optional findIntersectPointInNormalDirection( - Vector3D in, Vector3D normal, SmallBodyModel smallBodyModel) { - - double size = Math.max(smallBodyModel.getBoundingBoxDiagonalLength(), in.getNorm()); - - // First do the intersection from "below" - Vector3D startBottom = in.subtract(normal.scalarMultiply(size)); - double[] out1 = new double[3]; - long cellId1 = - smallBodyModel.computeRayIntersection(startBottom.toArray(), normal.toArray(), out1); - - // Then do the intersection from on "top" - Vector3D startTop = in.add(normal.scalarMultiply(size)); - double[] out2 = new double[3]; - long cellId2 = - smallBodyModel.computeRayIntersection(startTop.toArray(), normal.negate().toArray(), out2); - - if (cellId1 >= 0 && cellId2 >= 0) { - Vector3D out1V = new Vector3D(out1); - Vector3D out2V = new Vector3D(out2); - - Vector3D inSubOut1 = in.subtract(out1V); - Vector3D inSubOut2 = in.subtract(out2V); - // If both intersected, take the closest - if (inSubOut1.dotProduct(inSubOut1) < inSubOut2.dotProduct(inSubOut2)) - return Optional.of(out1V); - else return Optional.of(out2V); - } - if (cellId1 >= 0) return Optional.of(new Vector3D(out1)); - - if (cellId2 >= 0) return Optional.of(new Vector3D(out2)); - - return Optional.empty(); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("logFile") - .hasArg() - .argName("path") - .desc("If present, save screen output to .") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - - sb = new StringBuilder(); - sb.append("If specified, the program first computes the optimal rotation of "); - sb.append("-model so that it best matches -reference prior "); - sb.append("to computing the error between them. Prior to computing the errors, "); - sb.append("-model is transformed to -reference using this optimal "); - sb.append("rotation. This results in an error unbiased by a possible rotation between "); - sb.append("the two models."); - options.addOption(Option.builder("computeOptimalRotation").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("If specified, the program first computes the optimal translation of "); - sb.append("-model so that it best matches -reference prior to "); - sb.append("computing the error between them. Prior to computing the errors, -model "); - sb.append("is transformed to the -reference using this optimal translation. "); - sb.append("This results in an error unbiased by a possible translation offset between "); - sb.append("the two models."); - options.addOption(Option.builder("computeOptimalTranslation").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("If specified, the program first computes the optimal translation and "); - sb.append("rotation of -model so that it best matches -reference "); - sb.append("prior to computing the error between them. Prior to computing the errors, "); - sb.append("-model is transformed to -reference using this optimal "); - sb.append("translation and rotation. This results in an error unbiased by a possible "); - sb.append("translation offset or rotation between the two models."); - options.addOption( - Option.builder("computeOptimalRotationAndTranslation").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("If specified, the program computes the error in the vertical direction "); - sb.append("(by fitting a plane to -model) and saves it to . This option "); - sb.append("only produces meaningful results for digital terrain models to which a plane "); - sb.append("can be fit."); - options.addOption( - Option.builder("computeVerticalError") - .hasArg() - .argName("path") - .desc(sb.toString()) - .build()); - - sb = new StringBuilder(); - sb.append("Use the normal to a plane fit to the model when computing distances to the "); - sb.append("reference. If this option is absent, a plane is fit to the entire model. If "); - sb.append("present, only points within a distance of radius will be used to construct the "); - sb.append("plane. Recommended value is ~5% of the body radius for a global model. Units "); - sb.append("are units of the shape model."); - options.addOption(Option.builder("fitPlaneRadius").hasArg().desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Limit the distances (described in -savePlateDiff) used in calculating the mean "); - sb.append("distance and RMS distance to the closest fraction of all distances."); - options.addOption(Option.builder("limitClosestPoints").hasArg().desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Max number of control points to use in optimization. Default is 2000."); - options.addOption( - Option.builder("maxNumberControlPoints").hasArg().desc(sb.toString()).build()); - - options.addOption( - Option.builder("model") - .required() - .hasArg() - .argName("path") - .desc( - "Required. Point cloud/shape file to compare to reference shape. Valid formats are " - + "anything that can be read by the PointCloudFormatConverter.") - .build()); - options.addOption( - Option.builder("reference") - .required() - .hasArg() - .argName("path") - .desc( - "Required. Reference shape file. Valid formats are FITS, ICQ, OBJ, PLT, PLY, or VTK.") - .build()); - - sb = new StringBuilder(); - sb.append("Save the rotated and/or translated -model to . "); - sb.append("This option requires one of -computeOptimalRotation, -computeOptimalTranslation "); - sb.append("or -computeOptimalRotationAndTranslation to be specified."); - options.addOption( - Option.builder("saveOptimalShape").hasArg().argName("path").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Save to a file specified by the distances (in same units as the shape "); - sb.append("models) between each plate center of -model to the closest point in "); - sb.append("-reference. The number of lines in the file equals the number of plates in "); - sb.append("the first shape model with each line containing the distance of that plate "); - sb.append("to the closest point in the second shape model."); - options.addOption( - Option.builder("savePlateDiff").hasArg().argName("path").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Save to a file specified by the index of the closest plate in "); - sb.append("second (reference) shape model to the center of each plate in the first model. "); - sb.append("The format of each line is "); - sb.append("\n\tplate index, closest reference plate index, distance\n"); - sb.append("Only valid when -model is a shape model with facet information."); - options.addOption( - Option.builder("savePlateIndex").hasArg().argName("path").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Save output of lidar-optimize to (JSON format file)"); - options.addOption( - Option.builder("saveTransformationFile") - .hasArg() - .argName("path") - .desc(sb.toString()) - .build()); - - sb = new StringBuilder(); - sb.append("Directory to store temporary files. It will be created if it does not exist. "); - sb.append("Default is the current working directory."); - options.addOption( - Option.builder("tmpDir").hasArg().argName("path").desc(sb.toString()).build()); - - sb = new StringBuilder(); - sb.append("Use all points in -model when attempting to fit to "); - sb.append("-reference. The default behavior is to use only points which overlap the "); - sb.append("reference model."); - options.addOption(Option.builder("useAllPoints").desc(sb.toString()).build()); - return options; - } - - public static void main(String[] args) { - - TerrasaurTool defaultOBJ = new CompareOBJ(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - double limitClosestPoints = - cl.hasOption("limitClosestPoints") - ? Double.parseDouble(cl.getOptionValue("limitClosestPoints")) - : 1.0; - limitClosestPoints = Math.max(0, Math.min(1, limitClosestPoints)); - - boolean saveOptimalShape = cl.hasOption("saveOptimalShape"); - boolean computeOptimalTranslation = - cl.hasOption("computeOptimalRotationAndTranslation") - || cl.hasOption("computeOptimalTranslation"); - boolean computeOptimalRotation = - cl.hasOption("computeOptimalRotationAndTranslation") - || cl.hasOption("computeOptimalRotation"); - final boolean computeHorizontalError = false; - boolean useOverlappingPoints = !cl.hasOption("useAllPoints"); - double planeRadius = - cl.hasOption("fitPlaneRadius") - ? Double.parseDouble(cl.getOptionValue("fitPlaneRadius")) - : 0; - int maxNumberOfControlPoints = - cl.hasOption("maxNumberControlPoints") - ? Integer.parseInt(cl.getOptionValue("maxNumberControlPoints")) - : 2000; - int npoints = -1; - double size = 0; - String closestDiffFile = - cl.hasOption("savePlateDiff") ? cl.getOptionValue("savePlateDiff") : null; - String closestIndexFile = - cl.hasOption("savePlateIndex") ? cl.getOptionValue("savePlateIndex") : null; - String optimalShapeFile = saveOptimalShape ? cl.getOptionValue("saveOptimalShape") : null; - String verticalDiffFile = - cl.hasOption("computeVerticalError") ? cl.getOptionValue("computeVerticalError") : null; - String transformationFile = - cl.hasOption("saveTransformationFile") ? cl.getOptionValue("saveTransformationFile") : null; - String tmpdir = cl.hasOption("tmpDir") ? cl.getOptionValue("tmpDir") : "."; - - String infile1 = cl.getOptionValue("model"); - String infile2 = cl.getOptionValue("reference"); - - NativeLibraryLoader.loadSpiceLibraries(); - NativeLibraryLoader.loadVtkLibraries(); - - CompareOBJ compareOBJ = new CompareOBJ(infile1, infile2); - compareOBJ.setTmpdir(tmpdir); - - if (computeOptimalTranslation || computeOptimalRotation) { - compareOBJ.computeOptimalTransformationToTarget( - computeOptimalTranslation, - computeOptimalRotation, - maxNumberOfControlPoints, - useOverlappingPoints, - transformationFile); - - compareOBJ.transformPolyData(saveOptimalShape, optimalShapeFile); - } - - // note: this will never be called - if (computeHorizontalError) { - try { - compareOBJ.assessHorizontalAccuracy(npoints, size); - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - try { - compareOBJ.computeDifferences( - closestDiffFile, closestIndexFile, verticalDiffFile, limitClosestPoints, planeRadius); - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - } } diff --git a/src/main/java/terrasaur/apps/CreateSBMTStructure.java b/src/main/java/terrasaur/apps/CreateSBMTStructure.java index f0f44e7..5b14972 100644 --- a/src/main/java/terrasaur/apps/CreateSBMTStructure.java +++ b/src/main/java/terrasaur/apps/CreateSBMTStructure.java @@ -45,103 +45,98 @@ import vtk.vtkPolyData; public class CreateSBMTStructure implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + 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 CreateSBMTStructure() {} + /** + * This doesn't need to be private, or even declared, but you might want to if you have other + * constructors. + */ + private CreateSBMTStructure() {} - @Override - public String shortDescription() { - return "Construct ellipses from user-defined points on an image."; - } + @Override + public String shortDescription() { + return "Construct ellipses from user-defined points on an image."; + } - @Override - public String fullDescription(Options options) { - 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); - } + @Override + public String fullDescription(Options options) { + 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); + } - /** - * Create an Ellipse as an SBMT structure from three points. The first two points define the long - * axis and the third point defines the short axis. - * - * @param p1 First point - * @param p2 Second point - * @param p3 Third point - * @return An SBMT structure describing the ellipse - */ - private static SBMTEllipseRecord createRecord( - int id, String name, Vector3D p1, Vector3D p2, Vector3D p3) { - // Create a local coordinate system where X axis contains long axis and Y axis contains short - // axis + /** + * Create an Ellipse as an SBMT structure from three points. The first two points define the long + * axis and the third point defines the short axis. + * + * @param p1 First point + * @param p2 Second point + * @param p3 Third point + * @return An SBMT structure describing the ellipse + */ + private static SBMTEllipseRecord createRecord(int id, String name, Vector3D p1, Vector3D p2, Vector3D p3) { + // Create a local coordinate system where X axis contains long axis and Y axis contains short + // axis - Vector3D origin = p1.add(p2).scalarMultiply(0.5); - Vector3D X = p1.subtract(p2).normalize(); - Vector3D Y = p3.subtract(origin).normalize(); + Vector3D origin = p1.add(p2).scalarMultiply(0.5); + Vector3D X = p1.subtract(p2).normalize(); + Vector3D Y = p3.subtract(origin).normalize(); - // Create a rotation matrix to go from body fixed frame to this local coordinate system - Rotation globalToLocal = RotationUtils.IprimaryJsecondary(X, Y); + // Create a rotation matrix to go from body fixed frame to this local coordinate system + Rotation globalToLocal = RotationUtils.IprimaryJsecondary(X, Y); - // All of these vectors should have a Z coordinate of zero - Vector3D p1Local = globalToLocal.applyTo(p1); - Vector3D p2Local = globalToLocal.applyTo(p2); - Vector3D p3Local = globalToLocal.applyTo(p3); + // All of these vectors should have a Z coordinate of zero + Vector3D p1Local = globalToLocal.applyTo(p1); + Vector3D p2Local = globalToLocal.applyTo(p2); + Vector3D p3Local = globalToLocal.applyTo(p3); - // fit an ellipse to the three points on the plane - Vector2D a = new Vector2D(p1Local.getX(), p1Local.getY()); - Vector2D b = new Vector2D(p2Local.getX(), p2Local.getY()); - Vector2D c = new Vector2D(p3Local.getX(), p3Local.getY()); + // fit an ellipse to the three points on the plane + Vector2D a = new Vector2D(p1Local.getX(), p1Local.getY()); + Vector2D b = new Vector2D(p2Local.getX(), p2Local.getY()); + Vector2D c = new Vector2D(p3Local.getX(), p3Local.getY()); - Vector2D center = a.add(b).scalarMultiply(0.5); - double majorAxis = a.subtract(b).getNorm(); - double minorAxis = 2 * c.subtract(center).getNorm(); + Vector2D center = a.add(b).scalarMultiply(0.5); + double majorAxis = a.subtract(b).getNorm(); + double minorAxis = 2 * c.subtract(center).getNorm(); - double rotation = Math.atan2(b.getY() - a.getY(), b.getX() - a.getX()); - double flattening = (majorAxis - minorAxis) / majorAxis; + double rotation = Math.atan2(b.getY() - a.getY(), b.getX() - a.getX()); + double flattening = (majorAxis - minorAxis) / majorAxis; - ImmutableSBMTEllipseRecord.Builder record = - ImmutableSBMTEllipseRecord.builder() - .id(id) - .name(name) - .x(origin.getX()) - .y(origin.getY()) - .z(origin.getZ()) - .lat(origin.getDelta()) - .lon(origin.getAlpha()) - .radius(origin.getNorm()) - .slope(0) - .elevation(0) - .acceleration(0) - .potential(0) - .diameter(majorAxis) - .flattening(flattening) - .angle(rotation) - .color(Color.BLACK) - .dummy("") - .label(""); - return record.build(); - } + ImmutableSBMTEllipseRecord.Builder record = ImmutableSBMTEllipseRecord.builder() + .id(id) + .name(name) + .x(origin.getX()) + .y(origin.getY()) + .z(origin.getZ()) + .lat(origin.getDelta()) + .lon(origin.getAlpha()) + .radius(origin.getNorm()) + .slope(0) + .elevation(0) + .acceleration(0) + .potential(0) + .diameter(majorAxis) + .flattening(flattening) + .angle(rotation) + .color(Color.BLACK) + .dummy("") + .label(""); + return record.build(); + } - 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( -""" + 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 (X and Y) per line. The pixel coordinates are offsets from the image center. For example: @@ -158,125 +153,115 @@ Empty lines or lines beginning with # are ignored. Each set of three points are used to create the SBMT structures. The first two points are the long axis and the third is a location for the semi-minor axis.""") - .build()); - options.addOption( - Option.builder("objFile") - .required() - .hasArg() - .desc("Required. Name of OBJ shape file.") - .build()); - options.addOption( - Option.builder("output") - .required() - .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. This is still required with -spice, but only used as a template to create a new sum file.") - .build()); - return options; - } - - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new CreateSBMTStructure(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info("{} {}", ml.label, startupMessages.get(ml)); - - NativeLibraryLoader.loadSpiceLibraries(); - NativeLibraryLoader.loadVtkLibraries(); - - SumFile sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); - - try { - String objFile = cl.getOptionValue("objFile"); - vtkPolyData polyData = PolyDataUtil.loadShapeModel(objFile); - if (polyData == null) { - logger.error("Cannot read shape model {}!", objFile); - System.exit(0); - } - RangeFromSumFile rfsf = new RangeFromSumFile(sumFile, polyData); - - boolean flipX = cl.hasOption("flipX"); - boolean flipY = cl.hasOption("flipY"); - List intercepts = new ArrayList<>(); - List lines = - FileUtils.readLines(new File(cl.getOptionValue("input")), Charset.defaultCharset()); - for (String line : - lines.stream().filter(s -> !(s.isBlank() || s.strip().startsWith("#"))).toList()) { - String[] parts = line.split("\\s+"); - 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 entry = rfsf.findIntercept(ix, iy); - long cellID = entry.getKey(); - if (cellID > -1) intercepts.add(entry.getValue()); - } - - logger.info("Found {} sets of points", intercepts.size() / 3); - - List records = new ArrayList<>(); - for (int i = 0; i < intercepts.size(); i += 3) { - - // p1 and p2 define the long axis of the ellipse - Vector3D p1 = intercepts.get(i); - Vector3D p2 = intercepts.get(i + 1); - - // p3 lies on the short axis - Vector3D p3 = intercepts.get(i + 2); - - SBMTEllipseRecord record = - createRecord(i / 3, String.format("Ellipse %d", i / 3), p1, p2, p3); - records.add(record); - } - - try (PrintWriter pw = new PrintWriter(cl.getOptionValue("output"))) { - for (SBMTEllipseRecord record : records) pw.println(record.toString()); - } - logger.info("Wrote {}", cl.getOptionValue("output")); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - throw new RuntimeException(e); + .build()); + options.addOption(Option.builder("objFile") + .required() + .hasArg() + .desc("Required. Name of OBJ shape file.") + .build()); + options.addOption(Option.builder("output") + .required() + .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. This is still required with -spice, but only used as a template to create a new sum file.") + .build()); + return options; } - logger.info("Finished"); - } + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new CreateSBMTStructure(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) logger.info("{} {}", ml.label, startupMessages.get(ml)); + + NativeLibraryLoader.loadSpiceLibraries(); + NativeLibraryLoader.loadVtkLibraries(); + + SumFile sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); + + try { + String objFile = cl.getOptionValue("objFile"); + vtkPolyData polyData = PolyDataUtil.loadShapeModel(objFile); + if (polyData == null) { + logger.error("Cannot read shape model {}!", objFile); + System.exit(0); + } + RangeFromSumFile rfsf = new RangeFromSumFile(sumFile, polyData); + + boolean flipX = cl.hasOption("flipX"); + boolean flipY = cl.hasOption("flipY"); + List intercepts = new ArrayList<>(); + List lines = FileUtils.readLines(new File(cl.getOptionValue("input")), Charset.defaultCharset()); + for (String line : lines.stream() + .filter(s -> !(s.isBlank() || s.strip().startsWith("#"))) + .toList()) { + String[] parts = line.split("\\s+"); + 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 entry = rfsf.findIntercept(ix, iy); + long cellID = entry.getKey(); + if (cellID > -1) intercepts.add(entry.getValue()); + } + + logger.info("Found {} sets of points", intercepts.size() / 3); + + List records = new ArrayList<>(); + for (int i = 0; i < intercepts.size(); i += 3) { + + // p1 and p2 define the long axis of the ellipse + Vector3D p1 = intercepts.get(i); + Vector3D p2 = intercepts.get(i + 1); + + // p3 lies on the short axis + Vector3D p3 = intercepts.get(i + 2); + + SBMTEllipseRecord record = createRecord(i / 3, String.format("Ellipse %d", i / 3), p1, p2, p3); + records.add(record); + } + + try (PrintWriter pw = new PrintWriter(cl.getOptionValue("output"))) { + for (SBMTEllipseRecord record : records) pw.println(record.toString()); + } + logger.info("Wrote {}", cl.getOptionValue("output")); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + throw new RuntimeException(e); + } + + logger.info("Finished"); + } } diff --git a/src/main/java/terrasaur/apps/DSK2OBJ.java b/src/main/java/terrasaur/apps/DSK2OBJ.java index 3d5ac21..4cf242f 100644 --- a/src/main/java/terrasaur/apps/DSK2OBJ.java +++ b/src/main/java/terrasaur/apps/DSK2OBJ.java @@ -43,176 +43,168 @@ import terrasaur.templates.TerrasaurTool; public class DSK2OBJ implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Create an OBJ from a DSK."; - } - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = "\nCreate an OBJ from a DSK.\n"; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("body") - .hasArg() - .desc( - "If present, convert shape for named body. Default is to use the first body in the DSK.") - .build()); - options.addOption( - Option.builder("dsk").hasArg().required().desc("Required. Name of input DSK.").build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption(Option.builder("obj").hasArg().desc("Name of output OBJ.").build()); - options.addOption( - Option.builder("printBodies") - .desc("If present, print bodies and surface ids in DSK.") - .build()); - options.addOption( - Option.builder("surface") - .hasArg() - .desc( - "If present, use specified surface id. Default is to use the first surface id for the body.") - .build()); - return options; - } - - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new DSK2OBJ(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - System.loadLibrary("JNISpice"); - - String dskName = cl.getOptionValue("dsk"); - File dskFile = new File(dskName); - if (!dskFile.exists()) { - logger.warn("Input DSK " + dskName + "does not exist!"); - System.exit(0); + @Override + public String shortDescription() { + return "Create an OBJ from a DSK."; } - try { - DSK dsk = DSK.openForRead(dskName); - Body[] bodies = dsk.getBodies(); + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = "\nCreate an OBJ from a DSK.\n"; + return TerrasaurTool.super.fullDescription(options, header, footer); + } - if (cl.hasOption("printBodies")) { - logger.info("found bodies and surface ids:"); - for (int i = 0; i < bodies.length; i++) { - Body b = bodies[i]; - Surface[] surfaces = dsk.getSurfaces(b); - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%d) %s", i, b.getName())); - for (Surface s : surfaces) sb.append(String.format(" %d", s.getIDCode())); - logger.info(sb.toString()); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("body") + .hasArg() + .desc("If present, convert shape for named body. Default is to use the first body in the DSK.") + .build()); + options.addOption(Option.builder("dsk") + .hasArg() + .required() + .desc("Required. Name of input DSK.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption( + Option.builder("obj").hasArg().desc("Name of output OBJ.").build()); + options.addOption(Option.builder("printBodies") + .desc("If present, print bodies and surface ids in DSK.") + .build()); + options.addOption(Option.builder("surface") + .hasArg() + .desc("If present, use specified surface id. Default is to use the first surface id for the body.") + .build()); + return options; + } + + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new DSK2OBJ(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + System.loadLibrary("JNISpice"); + + String dskName = cl.getOptionValue("dsk"); + File dskFile = new File(dskName); + if (!dskFile.exists()) { + logger.warn("Input DSK " + dskName + "does not exist!"); + System.exit(0); } - } - Body b = cl.hasOption("body") ? new Body(cl.getOptionValue("body")) : bodies[0]; - boolean missingBody = true; - for (Body body : bodies) { - if (b.equals(body)) { - missingBody = false; - break; - } - } - if (missingBody) { - logger.warn(String.format("Body %s not found in DSK! Valid bodies are:", b.getName())); - for (Body body : bodies) logger.warn(body.getName()); - System.exit(0); - } + try { + DSK dsk = DSK.openForRead(dskName); + Body[] bodies = dsk.getBodies(); - Surface[] surfaces = dsk.getSurfaces(b); - Surface s = - cl.hasOption("surface") - ? new Surface(Integer.parseInt(cl.getOptionValue("surface")), b) - : surfaces[0]; - boolean missingSurface = true; - for (Surface surface : surfaces) { - if (s.equals(surface)) { - missingSurface = false; - break; - } - } - if (missingSurface) { - logger.warn( - String.format( - "Surface %d for body %s not found in DSK! Valid surfaces are:", - s.getIDCode(), b.getName())); - for (Surface surface : surfaces) logger.warn(Integer.toString(surface.getIDCode())); - System.exit(0); - } - - DLADescriptor dladsc = dsk.beginBackwardSearch(); - boolean found = true; - while (found) { - - DSKDescriptor dskdsc = dsk.getDSKDescriptor(dladsc); - - if (b.getIDCode() == dskdsc.getCenterID() && s.getIDCode() == dskdsc.getSurfaceID()) { - - // number of plates and vertices - int[] np = new int[1]; - int[] nv = new int[1]; - CSPICE.dskz02(dsk.getHandle(), dladsc.toArray(), nv, np); - - double[][] vertices = CSPICE.dskv02(dsk.getHandle(), dladsc.toArray(), 1, nv[0]); - int[][] plates = CSPICE.dskp02(dsk.getHandle(), dladsc.toArray(), 1, np[0]); - - if (cl.hasOption("obj")) { - try (PrintWriter pw = new PrintWriter(cl.getOptionValue("obj"))) { - for (double[] v : vertices) { - pw.printf("v %20.16f %20.16f %20.16f\r\n", v[0], v[1], v[2]); - } - for (int[] p : plates) { - pw.printf("f %d %d %d\r\n", p[0], p[1], p[2]); - } - logger.info( - String.format( - "Wrote %d vertices and %d plates to %s for body %d surface %d", - nv[0], - np[0], - cl.getOptionValue("obj"), - dskdsc.getCenterID(), - dskdsc.getSurfaceID())); - } catch (FileNotFoundException e) { - logger.warn(e.getLocalizedMessage()); + if (cl.hasOption("printBodies")) { + logger.info("found bodies and surface ids:"); + for (int i = 0; i < bodies.length; i++) { + Body b = bodies[i]; + Surface[] surfaces = dsk.getSurfaces(b); + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%d) %s", i, b.getName())); + for (Surface s : surfaces) sb.append(String.format(" %d", s.getIDCode())); + logger.info(sb.toString()); + } } - } - } - found = dsk.hasPrevious(dladsc); - if (found) { - dladsc = dsk.getPrevious(dladsc); - } - } + Body b = cl.hasOption("body") ? new Body(cl.getOptionValue("body")) : bodies[0]; + boolean missingBody = true; + for (Body body : bodies) { + if (b.equals(body)) { + missingBody = false; + break; + } + } + if (missingBody) { + logger.warn(String.format("Body %s not found in DSK! Valid bodies are:", b.getName())); + for (Body body : bodies) logger.warn(body.getName()); + System.exit(0); + } - } catch (SpiceException e) { - logger.warn(e.getLocalizedMessage()); + Surface[] surfaces = dsk.getSurfaces(b); + Surface s = cl.hasOption("surface") + ? new Surface(Integer.parseInt(cl.getOptionValue("surface")), b) + : surfaces[0]; + boolean missingSurface = true; + for (Surface surface : surfaces) { + if (s.equals(surface)) { + missingSurface = false; + break; + } + } + if (missingSurface) { + logger.warn(String.format( + "Surface %d for body %s not found in DSK! Valid surfaces are:", s.getIDCode(), b.getName())); + for (Surface surface : surfaces) logger.warn(Integer.toString(surface.getIDCode())); + System.exit(0); + } + + DLADescriptor dladsc = dsk.beginBackwardSearch(); + boolean found = true; + while (found) { + + DSKDescriptor dskdsc = dsk.getDSKDescriptor(dladsc); + + if (b.getIDCode() == dskdsc.getCenterID() && s.getIDCode() == dskdsc.getSurfaceID()) { + + // number of plates and vertices + int[] np = new int[1]; + int[] nv = new int[1]; + CSPICE.dskz02(dsk.getHandle(), dladsc.toArray(), nv, np); + + double[][] vertices = CSPICE.dskv02(dsk.getHandle(), dladsc.toArray(), 1, nv[0]); + int[][] plates = CSPICE.dskp02(dsk.getHandle(), dladsc.toArray(), 1, np[0]); + + if (cl.hasOption("obj")) { + try (PrintWriter pw = new PrintWriter(cl.getOptionValue("obj"))) { + for (double[] v : vertices) { + pw.printf("v %20.16f %20.16f %20.16f\r\n", v[0], v[1], v[2]); + } + for (int[] p : plates) { + pw.printf("f %d %d %d\r\n", p[0], p[1], p[2]); + } + logger.info(String.format( + "Wrote %d vertices and %d plates to %s for body %d surface %d", + nv[0], + np[0], + cl.getOptionValue("obj"), + dskdsc.getCenterID(), + dskdsc.getSurfaceID())); + } catch (FileNotFoundException e) { + logger.warn(e.getLocalizedMessage()); + } + } + } + + found = dsk.hasPrevious(dladsc); + if (found) { + dladsc = dsk.getPrevious(dladsc); + } + } + + } catch (SpiceException e) { + logger.warn(e.getLocalizedMessage()); + } } - } } diff --git a/src/main/java/terrasaur/apps/DifferentialVolumeEstimator.java b/src/main/java/terrasaur/apps/DifferentialVolumeEstimator.java index 8d00326..9188882 100644 --- a/src/main/java/terrasaur/apps/DifferentialVolumeEstimator.java +++ b/src/main/java/terrasaur/apps/DifferentialVolumeEstimator.java @@ -79,939 +79,945 @@ import vtk.vtkPolyDataWriter; * local coordinate system, but the origin may be translated and there may be a rotation applied * about the Z axis. * - * + * * @author Hari.Nair@jhuapl.edu * */ public class DifferentialVolumeEstimator implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private DifferentialVolumeEstimator() {} + private DifferentialVolumeEstimator() {} - - @Override - public String shortDescription() { - return "Find volume difference between two shape models."; - } - - // degree of polynomial used to fit surface - private final int POLYNOMIAL_DEGREE = 2; - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = "\nThis program finds the volume difference between a shape model and a reference surface. " - +"The reference surface can either be another shape model or a degree "+POLYNOMIAL_DEGREE+" fit to a set of supplied points. "+ - "A local coordinate system is derived from the reference surface. The heights of the shape and reference at "+ - "each grid point are reported. "; - return TerrasaurTool.super.fullDescription(options, header, footer); - - } - - - - /** input shape model */ - private vtkPolyData globalPolyData; - /** shape model in native coordinates */ - private SmallBodyModel nativeSBM; - /** number of radial profiles */ - private Integer numProfiles; - - public void setNumProfiles(Integer numProfiles) { - this.numProfiles = numProfiles; - } - - /** true if local +Z is aligned with the center radial direction */ - private boolean radialUp; - - public void setRadialUp(boolean radialUp) { - this.radialUp = radialUp; - } - - /** reference points in global coordinates */ - private List referencePoints; - - public void setReferencePoints(List referencePoints) { - this.referencePoints = referencePoints; - } - - /** reference model in global coordinates */ - private vtkPolyData referencePolyData; - - public void setReferencePolyData(vtkPolyData referencePolyData) { - this.referencePolyData = referencePolyData; - } - - /** Reference shape in native coordinates */ - private SmallBodyModel referenceSBM; - /** reference surface in native coordinates */ - private FitSurface referenceSurface; - - private double gridSpacing; - private double gridHalfExtent; - - /** global coordinates of the highest point of the shape model */ - private Vector3D highPoint; - /** global coordinates of the lowest point of the shape model */ - private Vector3D lowPoint; - - /** this plane converts coordinates from native to global and back */ - private FitPlane plane; - - /** Inner edge of the ROI */ - private Path2D.Double roiInner; - /** Outer edge of the ROI */ - private Path2D.Double roiOuter; - - // local grid is in the same plane as native grid but is translated and rotated - private Entry nativeToLocal; - - // the origin of the local coordinate system, in global coordinates - private enum ORIGIN { - MIN_HEIGHT, MAX_HEIGHT, CUSTOM, DEFAULT - } - - public Vector3D nativeToLocal(Vector3D nativeIJK) { - return nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue())); - } - - public Vector3D localToNative(Vector3D local) { - return nativeToLocal.getKey().applyInverseTo(local).add(nativeToLocal.getValue()); - } - - /** - * Set the inner boundary of the ROI - * - * @param filename file containing points in global coordinates - */ - public void setInnerROI(String filename) { - List points = readPointsFromFile(filename); - roiInner = createOutline(points); - } - - /** - * Set the outer boundary of the ROI - * - * @param filename file containing points in global coordinates - */ - public void setOuterROI(String filename) { - List points = readPointsFromFile(filename); - roiOuter = createOutline(points); - } - - /** - * Construct an outline on the local grid from a list of points in global coordinates. - * - * @param points points in global coordinates - * @return outline on local grid - */ - private Path2D.Double createOutline(List points) { - - Path2D.Double outline = new Path2D.Double(); - for (int i = 0; i < points.size(); i++) { - Vector3D nativeIJK = plane.globalToLocal(points.get(i)); - Vector3D localIJK = - nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue())); - if (i == 0) { - outline.moveTo(localIJK.getX(), localIJK.getY()); - } else { - outline.lineTo(localIJK.getX(), localIJK.getY()); - } - } - outline.closePath(); - - return outline; - } - - public DifferentialVolumeEstimator(vtkPolyData polyData) { - this.globalPolyData = polyData; - this.referencePolyData = null; - this.referencePoints = null; - this.numProfiles = 0; - this.radialUp = false; - } - - /** - * Get the height of the shape model above the reference plane - * - * @param x in native coordinates - * @param y in native coordinates - * @return height, or {@link Double#NaN} if no intersection found - */ - public double getHeight(double x, double y) { - - double height = Double.NaN; - double[] origin = {x, y, 0}; - double[] direction = {0, 0, 1}; - double[] intersect = new double[3]; - - long cellID = nativeSBM.computeRayIntersection(origin, direction, intersect); - if (cellID < 0) { - direction[2] = -1; - cellID = nativeSBM.computeRayIntersection(origin, direction, intersect); - } - if (cellID >= 0) - height = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect)); - - if (Double.isNaN(height)) - return Double.NaN; - - return height; - } - - /** - * Get the height of the reference surface above the reference plane. - * - * @param x in native coordinates - * @param y in native coordinates - * @return height of surface at (x,y) above reference plane - */ - public double getRefHeight(double x, double y) { - - double[] origin = {x, y, 0}; - double[] direction = {0, 0, 1}; - double[] intersect; - - double refHeight = Double.NaN; - if (referenceSBM != null) { - intersect = new double[3]; - - long cellID = referenceSBM.computeRayIntersection(origin, direction, intersect); - if (cellID < 0) { - direction[2] = -1; - cellID = referenceSBM.computeRayIntersection(origin, direction, intersect); - } - if (cellID >= 0) { - refHeight = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect)); - } - } else { - refHeight = referenceSurface.value(x, y); - } - - if (Double.isNaN(refHeight)) - return Double.NaN; - - return refHeight; - } - - /** - * Create an array of grid points with heights - * - * @param gridHalfExtent half-size of grid - * @param gridSpacing spacing between points - * @return grid points sorted by x coordinate, then y - */ - private NavigableSet createGrid(double gridHalfExtent, double gridSpacing) { - Set localGrid = new HashSet<>(); - - for (double x = 0; x <= gridHalfExtent; x += gridSpacing) { - for (double y = 0; y <= gridHalfExtent; y += gridSpacing) { - localGrid.add(new Vector3D(x, y, 0)); - if (y != 0) - localGrid.add(new Vector3D(x, -y, 0)); - if (x != 0) - localGrid.add(new Vector3D(-x, y, 0)); - if (x != 0 && y != 0) - localGrid.add(new Vector3D(-x, -y, 0)); - } - } - this.gridSpacing = gridSpacing; - this.gridHalfExtent=gridHalfExtent; - - GridPoint highGridPoint = null; - GridPoint lowGridPoint = null; - NavigableSet gridPoints = new TreeSet<>(); - for (Vector3D localPoint : localGrid) { - GridPoint gp = new GridPoint(localPoint); - gridPoints.add(gp); - if (Double.isFinite(gp.height)) { - if (highGridPoint == null || highGridPoint.height < gp.height) - highGridPoint = gp; - if (lowGridPoint == null || lowGridPoint.height > gp.height) - lowGridPoint = gp; - } - } - - if (highGridPoint != null) - highPoint = highGridPoint.globalIJK; - if (lowGridPoint != null) - lowPoint = lowGridPoint.globalIJK; - - return gridPoints; - } - - /** - * Create the reference surface, either from a fit to a set of points or from an input shape - * model. - * - * @param localOriginInGlobalCoordinates local origin in global coordinates - */ - public void createReference(Vector3D localOriginInGlobalCoordinates) { - - double[] pt = new double[3]; - - if (referencePolyData != null) { - if (referencePoints != null) { - logger.warn( - "Both -referenceList and -referenceShape were specified. Reference surface will be set to argument of -referenceShape."); - } - referencePoints = new ArrayList<>(); - for (int i = 0; i < referencePolyData.GetNumberOfPoints(); i++) { - referencePolyData.GetPoint(i, pt); - referencePoints.add(new Vector3D(pt)); - } - } - - // this is the best fit plane to the reference points. It can convert points in input - // (global) coordinates to native coordinates and vice versa - plane = new FitPlane(referencePoints); - - // set the +Z direction for the local plane - Vector3D referenceNormal = radialUp ? plane.getTransform().getValue() : Vector3D.PLUS_K; - - // check if the plane normal is pointing in the same direction as the reference normal. If not, - // flip the plane - Pair transform = plane.getTransform(); - Vector3D planeNormal = transform.getKey().applyInverseTo(referenceNormal); - if (planeNormal.dotProduct(referenceNormal) < 0) - plane = plane.reverseNormal(); - - // create the SmallBodyModel for the shape to evaluate - vtkPolyData nativePolyData = new vtkPolyData(); - nativePolyData.DeepCopy(globalPolyData); - vtkPoints points = nativePolyData.GetPoints(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - points.GetPoint(i, pt); - Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt)); - double[] data = nativePoint.toArray(); - points.SetPoint(i, data); - } - nativeSBM = new SmallBodyModel(nativePolyData); - - // now define the reference shape/surface - if (referencePolyData != null) { - // create the SmallBodyModel for the reference shape - nativePolyData = new vtkPolyData(); - nativePolyData.DeepCopy(referencePolyData); - points = nativePolyData.GetPoints(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - points.GetPoint(i, pt); - Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt)); - double[] data = nativePoint.toArray(); - points.SetPoint(i, data); - } - - referenceSBM = new SmallBodyModel(nativePolyData); - } else { - // create the reference surface - List nativePoints = new ArrayList<>(); - for (Vector3D v : referencePoints) { - Vector3D nativePoint = plane.globalToLocal(v); - nativePoints.add(nativePoint); - } - - referenceSurface = new FitSurface(nativePoints, POLYNOMIAL_DEGREE); - } - - // create a rotation matrix to go from native to local (where the Z axis is the same for both - // and the X axis is aligned in the same direction as the global X axis) - Pair globalToLocalTransform = plane.getTransform(); - Vector3D kRow = Vector3D.PLUS_K; - Vector3D iRow = globalToLocalTransform.getKey().applyTo(Vector3D.PLUS_I); - Vector3D jRow = Vector3D.crossProduct(kRow, iRow).normalize(); - kRow = Vector3D.crossProduct(iRow, jRow).normalize(); - iRow = iRow.normalize(); - - Vector3D translateNativeToLocal = Vector3D.ZERO; - if (localOriginInGlobalCoordinates.getNorm() > 0) { - // translation to go from native to local (where localOriginInGlobalCoordinates defines 0,0 in - // the local frame) - Vector3D nativeOriginInGlobalCoordinates = plane.localToGlobal(Vector3D.ZERO); - Vector3D translateNativeToLocalInGlobalCoordinates = - localOriginInGlobalCoordinates.subtract(nativeOriginInGlobalCoordinates); - // TODO: check that the Z component is zero (it should be?) - translateNativeToLocal = - globalToLocalTransform.getKey().applyTo(translateNativeToLocalInGlobalCoordinates); - } - - Rotation rotateNativeToLocal = - MathConversions.toRotation(new RotationMatrixIJK(iRow.getX(), jRow.getX(), kRow.getX(), - iRow.getY(), jRow.getY(), kRow.getY(), iRow.getZ(), jRow.getZ(), kRow.getZ())); - - this.nativeToLocal = new AbstractMap.SimpleEntry<>(rotateNativeToLocal, translateNativeToLocal); - } - - /** - * The header for grid and profile CSV files. Each line begins with a # - * - * @param header string at beginning of header - * @return complete header - */ - public static String getHeader(String header) { - StringBuffer sb = new StringBuffer(); - sb.append(header); - sb.append("# Local X and Y are grid coordinates in the local reference frame\n"); - sb.append("# Angle is measured from the local X axis, in degrees\n"); - sb.append("# ROI flag is 1 if point is in the region of interest, 0 if not\n"); - sb.append("# Global X, Y, and Z are the local grid points in the global " - + " (input) reference system\n"); - sb.append("# Reference Height is the height of the reference model (or fit surface) above " - + "the local grid plane\n"); - sb.append("# Model Height is the height of the shape model above the local grid plane. " - + "NaN means there is no model intersection at this grid point.\n"); - sb.append("# Bin volume is the grid cell area times the model - reference height\n"); - sb.append("#\n"); - sb.append(String.format("%s, ", "Local X")); - sb.append(String.format("%s, ", "Local Y")); - sb.append(String.format("%s, ", "Angle")); - sb.append(String.format("%s, ", "ROI Flag")); - sb.append(String.format("%s, ", "Global X")); - sb.append(String.format("%s, ", "Global Y")); - sb.append(String.format("%s, ", "Global Z")); - sb.append(String.format("%s, ", "Reference Height")); - sb.append(String.format("%s, ", "Model Height")); - sb.append(String.format("%s, ", "Model - Reference")); - sb.append(String.format("%s", "Bin Volume")); - return sb.toString(); - } - - - /** - * The header for the sector CSV file. Each line begins with a # - * - * @param header string at beginning of header - * @return complete header - */ - public static String getSectorHeader(String header) { - StringBuilder sb = new StringBuilder(); - sb.append(header); - sb.append("# Angle is measured from the local X axis, in degrees\n"); - sb.append( - "# Sector volume is the grid cell area times the model - reference height summed over all grid cells in the ROI.\n"); - sb.append("#\n"); - sb.append(String.format("%s, ", "Index")); - sb.append(String.format("%s, ", "Start angle (degrees)")); - sb.append(String.format("%s, ", "Stop angle (degrees)")); - sb.append(String.format("%s, ", "Sector Volume above reference surface")); - sb.append(String.format("%s, ", "Sector Volume below reference surface")); - sb.append(String.format("%s", "Total Sector Volume")); - - return sb.toString(); - } - - private String toCSV(GridPoint gp) { - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%f, ", gp.localIJK.getX())); - sb.append(String.format("%f, ", gp.localIJK.getY())); - - double angle = Math.toDegrees(Math.atan2(gp.localIJK.getY(), gp.localIJK.getX())); - if (angle < 0) - angle += 360; - sb.append(String.format("%f, ", angle)); - sb.append(String.format("%d, ", isInsideROI(gp.localIJK) ? 1 : 0)); - - sb.append(String.format("%f, ", gp.globalIJK.getX())); - sb.append(String.format("%f, ", gp.globalIJK.getY())); - sb.append(String.format("%f, ", gp.globalIJK.getZ())); - - sb.append(String.format("%g, ", gp.referenceHeight)); - sb.append(String.format("%g, ", gp.height)); - sb.append(String.format("%g, ", gp.differentialHeight)); - sb.append(String.format("%g", gridSpacing * gridSpacing * gp.differentialHeight)); - - return sb.toString(); - } - - /** - * - * @param localIJ Point on the local grid. The Z coordinate is ignored - * @return true if the point is inside the outer boundary and outside the inner boundary. If the - * outer boundary is null then all points are considered to be inside the outer boundary. If the - * inner boundary is null all points are considered to be outside the inner boundary. - */ - private boolean isInsideROI(Vector3D localIJ) { - Point2D thisPoint = new Point2D.Double(localIJ.getX(), localIJ.getY()); - boolean insideROI = roiOuter == null || roiOuter.contains(thisPoint); - if (roiInner != null) { - if (insideROI && roiInner.contains(thisPoint)) - insideROI = false; - } - return insideROI; - } - - /** - * Write out a VTK file with the local grid points. Useful for a sanity check. - * - * @param gridPointsList grid points - * @param profilesMap grid points along each profile - * @param sectorsMap grid points within each sector - * @param vtkFile file to write - */ - private void writeReferenceVTK(Collection gridPointsList, - Map> profilesMap, - Map> sectorsMap, String vtkFile) { - - Map roiMap = new HashMap<>(); - Map profileMap = new HashMap<>(); - Map sectorMap = new HashMap<>(); - - for (GridPoint gp : gridPointsList) { - Vector3D localIJK = gp.localIJK; - Vector3D nativeIJK = - nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); - Vector3D globalIJK = plane.localToGlobal(nativeIJK); - roiMap.put(globalIJK, isInsideROI(gp.localIJK)); - profileMap.put(globalIJK, 0); - sectorMap.put(globalIJK, 0); - } - - for (int i : profilesMap.keySet()) { - for (GridPoint gp : profilesMap.get(i)) { - Vector3D localIJK = gp.localIJK; - Vector3D nativeIJK = - nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); - Vector3D globalIJK = plane.localToGlobal(nativeIJK); - profileMap.put(globalIJK, i + 1); - } - } - - for (int i : sectorsMap.keySet()) { - for (GridPoint gp : sectorsMap.get(i)) { - Vector3D localIJK = gp.localIJK; - Vector3D nativeIJK = - nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); - Vector3D globalIJK = plane.localToGlobal(nativeIJK); - sectorMap.put(globalIJK, i + 1); - } - } - - vtkDoubleArray insideROI = new vtkDoubleArray(); - insideROI.SetName("Inside ROI"); - - vtkDoubleArray profiles = new vtkDoubleArray(); - profiles.SetName("Profiles"); - - vtkDoubleArray sectors = new vtkDoubleArray(); - sectors.SetName("Sectors"); - - vtkPoints pointsXYZ = new vtkPoints(); - for (Vector3D point : roiMap.keySet()) { - double[] array = point.toArray(); - pointsXYZ.InsertNextPoint(array); - insideROI.InsertNextValue(roiMap.get(point) ? 1 : 0); - profiles.InsertNextValue(profileMap.get(point)); - sectors.InsertNextValue(sectorMap.get(point)); - } - - vtkPolyData polyData = new vtkPolyData(); - polyData.SetPoints(pointsXYZ); - polyData.GetPointData().AddArray(insideROI); - polyData.GetPointData().AddArray(profiles); - polyData.GetPointData().AddArray(sectors); - - vtkCellArray cells = new vtkCellArray(); - polyData.SetPolys(cells); - - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - vtkIdList idList = new vtkIdList(); - idList.InsertNextId(i); - cells.InsertNextCell(idList); - } - - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputData(polyData); - writer.SetFileName(vtkFile); - writer.SetFileTypeToBinary(); - writer.Update(); - } - - /** - * Write the local grid out to a file - * - * @param gridPoints grid points - * @param header file header - * @param outputBasename CSV file to write - */ - private void writeGridCSV(Collection gridPoints, String header, String outputBasename) { - String csvFile = outputBasename + "_grid.csv"; - - try (PrintWriter pw = new PrintWriter(csvFile)) { - pw.println(getHeader(header)); - for (GridPoint gp : gridPoints) - pw.println(toCSV(gp)); - } catch (FileNotFoundException e) { - logger.warn("Can't write " + csvFile); - logger.warn(e.getLocalizedMessage()); - } - } - - /** - * Write profiles to file - * - * @param gridPoints grid points - * @param header file header - * @param outputBasename CSV file to write - */ - private Map> writeProfileCSV(Collection gridPoints, - String header, String outputBasename) { - Map> profileMap = new HashMap<>(); - - if (numProfiles == 0) - return profileMap; - - // sort grid points into radial bins - NavigableMap> radialMap = new TreeMap<>(); - for (GridPoint gp : gridPoints) { - double radius = gp.localIJK.getNorm() / gridSpacing; - int key = (int) radius; - Set set = radialMap.computeIfAbsent(key, k -> new HashSet<>()); - set.add(gp); - } - - final double deltaAngle = 2 * Math.PI / numProfiles; - for (int i = 0; i < numProfiles; i++) { - - Collection profileGridPoints = new HashSet<>(); - profileMap.put(i, profileGridPoints); - - double angle = deltaAngle * i; - String csvFile = String.format("%s_profile_%03d.csv", outputBasename, - (int) Math.round(Math.toDegrees(angle))); - - try (PrintWriter pw = new PrintWriter(csvFile)) { - pw.println(getHeader(header)); - for (int bin : radialMap.keySet()) { - - // stop profile at grid edge - double thisX = Math.abs(Math.cos(angle) * bin) * gridSpacing; - if (thisX > gridHalfExtent) continue; - double thisY = Math.abs(Math.sin(angle) * bin) * gridSpacing; - if (thisY > gridHalfExtent) continue; - - // sort points in this radial bin by angular distance from profile angle - NavigableSet sortedByAngle = new TreeSet<>((o1, o2) -> { - double angle1 = Math.atan2(o1.localIJK.getY(), o1.localIJK.getX()); - if (angle1 < 0) - angle1 += 2 * Math.PI; - double angle2 = Math.atan2(o2.localIJK.getY(), o2.localIJK.getX()); - if (angle2 < 0) - angle2 += 2 * Math.PI; - return Double.compare(Math.abs(angle1 - angle), Math.abs(angle2 - angle)); - }); - - sortedByAngle.addAll(radialMap.get(bin)); - - pw.println(toCSV(sortedByAngle.first())); - GridPoint thisPoint = sortedByAngle.first(); - if (Double.isFinite(thisPoint.differentialHeight)) - profileGridPoints.add(thisPoint); - } - } catch (FileNotFoundException e) { - logger.warn("Can't write {}", csvFile); - logger.warn(e.getLocalizedMessage()); - } - - } - - return profileMap; - } - - /** - * Write sector volumes to a file - * - * @param gridPoints grid points - * @param header file header - * @param outputBasename CSV file to write - */ - private Map> writeSectorCSV(Collection gridPoints, - String header, String outputBasename) { - - // grid points in each sector - Map> sectorMap = new HashMap<>(); - - if (numProfiles == 0) - return sectorMap; - - String csvFile = outputBasename + "_sector.csv"; - - NavigableMap aboveMap = new TreeMap<>(); - NavigableMap belowMap = new TreeMap<>(); - final double deltaAngle = 2 * Math.PI / numProfiles; - for (int i = 0; i < numProfiles; i++) { - aboveMap.put(i * deltaAngle, 0.); - belowMap.put(i * deltaAngle, 0.); - } - - // run through all the grid points and put them in the appropriate sector - double gridCellArea = gridSpacing * gridSpacing; - for (GridPoint gp : gridPoints) { - Vector3D localIJK = gp.localIJK; - double azimuth = Math.atan2(localIJK.getY(), localIJK.getX()); - if (azimuth < 0) - azimuth += 2 * Math.PI; - double key = aboveMap.floorKey(azimuth); - - int sector = (int) (key / deltaAngle); - Collection sectorGridPoints = sectorMap.computeIfAbsent(sector, k -> new HashSet<>()); - - if (isInsideROI(gp.localIJK)) { - double dv = gridCellArea * gp.differentialHeight; - if (Double.isFinite(dv)) { - if (dv > 0) { - aboveMap.compute(key, (k, value) -> value + dv); - } else { - belowMap.compute(key, (k, value) -> value + dv); - } - sectorGridPoints.add(gp); - } - } - } - - try (PrintWriter pw = new PrintWriter(csvFile)) { - pw.println(getSectorHeader(header)); - for (double azimuth : aboveMap.keySet()) { - StringBuffer sb = new StringBuffer(); - sb.append(String.format("%d, ", (int) (azimuth / deltaAngle))); - sb.append(String.format("%.2f, ", Math.toDegrees(azimuth))); - sb.append(String.format("%.2f, ", Math.toDegrees(azimuth + deltaAngle))); - sb.append(String.format("%e, ", aboveMap.get(azimuth))); - sb.append(String.format("%e, ", belowMap.get(azimuth))); - sb.append(String.format("%e", aboveMap.get(azimuth) + belowMap.get(azimuth))); - pw.println(sb); - } - - } catch (FileNotFoundException e) { - logger.warn("Can't write " + csvFile); - logger.warn(e.getLocalizedMessage()); - } - - return sectorMap; - - } - - private class GridPoint implements Comparable { - Vector3D localIJK; - Vector3D globalIJK; - double referenceHeight; - double height; - double differentialHeight; - - /** - * Create a grid point from an input location in local coordinates - * - * @param xy point in local coordinates. Z value is ignored. - */ - public GridPoint(Vector3D xy) { - this.localIJK = xy; - Vector3D nativeIJK = - nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); - globalIJK = plane.localToGlobal(nativeIJK); - referenceHeight = getRefHeight(nativeIJK.getX(), nativeIJK.getY()); - height = getHeight(nativeIJK.getX(), nativeIJK.getY()); - differentialHeight = height - referenceHeight; - } - - /** - * sort by the x coordinate on the local grid, then by the y coordinate. - */ @Override - public int compareTo(GridPoint o) { - int compare = Double.compare(localIJK.getX(), o.localIJK.getX()); - if (compare == 0) - compare = Double.compare(localIJK.getY(), o.localIJK.getY()); - return compare; + public String shortDescription() { + return "Find volume difference between two shape models."; } - } - private static List readPointsFromFile(String filename) { - List points = new ArrayList<>(); + // degree of polynomial used to fit surface + private final int POLYNOMIAL_DEGREE = 2; - try { - if (FilenameUtils.getExtension(filename).equalsIgnoreCase("vtk")) { + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = "\nThis program finds the volume difference between a shape model and a reference surface. " + + "The reference surface can either be another shape model or a degree " + POLYNOMIAL_DEGREE + + " fit to a set of supplied points. " + + "A local coordinate system is derived from the reference surface. The heights of the shape and reference at " + + "each grid point are reported. "; + return TerrasaurTool.super.fullDescription(options, header, footer); + } + + /** input shape model */ + private vtkPolyData globalPolyData; + /** shape model in native coordinates */ + private SmallBodyModel nativeSBM; + /** number of radial profiles */ + private Integer numProfiles; + + public void setNumProfiles(Integer numProfiles) { + this.numProfiles = numProfiles; + } + + /** true if local +Z is aligned with the center radial direction */ + private boolean radialUp; + + public void setRadialUp(boolean radialUp) { + this.radialUp = radialUp; + } + + /** reference points in global coordinates */ + private List referencePoints; + + public void setReferencePoints(List referencePoints) { + this.referencePoints = referencePoints; + } + + /** reference model in global coordinates */ + private vtkPolyData referencePolyData; + + public void setReferencePolyData(vtkPolyData referencePolyData) { + this.referencePolyData = referencePolyData; + } + + /** Reference shape in native coordinates */ + private SmallBodyModel referenceSBM; + /** reference surface in native coordinates */ + private FitSurface referenceSurface; + + private double gridSpacing; + private double gridHalfExtent; + + /** global coordinates of the highest point of the shape model */ + private Vector3D highPoint; + /** global coordinates of the lowest point of the shape model */ + private Vector3D lowPoint; + + /** this plane converts coordinates from native to global and back */ + private FitPlane plane; + + /** Inner edge of the ROI */ + private Path2D.Double roiInner; + /** Outer edge of the ROI */ + private Path2D.Double roiOuter; + + // local grid is in the same plane as native grid but is translated and rotated + private Entry nativeToLocal; + + // the origin of the local coordinate system, in global coordinates + private enum ORIGIN { + MIN_HEIGHT, + MAX_HEIGHT, + CUSTOM, + DEFAULT + } + + public Vector3D nativeToLocal(Vector3D nativeIJK) { + return nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue())); + } + + public Vector3D localToNative(Vector3D local) { + return nativeToLocal.getKey().applyInverseTo(local).add(nativeToLocal.getValue()); + } + + /** + * Set the inner boundary of the ROI + * + * @param filename file containing points in global coordinates + */ + public void setInnerROI(String filename) { + List points = readPointsFromFile(filename); + roiInner = createOutline(points); + } + + /** + * Set the outer boundary of the ROI + * + * @param filename file containing points in global coordinates + */ + public void setOuterROI(String filename) { + List points = readPointsFromFile(filename); + roiOuter = createOutline(points); + } + + /** + * Construct an outline on the local grid from a list of points in global coordinates. + * + * @param points points in global coordinates + * @return outline on local grid + */ + private Path2D.Double createOutline(List points) { + + Path2D.Double outline = new Path2D.Double(); + for (int i = 0; i < points.size(); i++) { + Vector3D nativeIJK = plane.globalToLocal(points.get(i)); + Vector3D localIJK = nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue())); + if (i == 0) { + outline.moveTo(localIJK.getX(), localIJK.getY()); + } else { + outline.lineTo(localIJK.getX(), localIJK.getY()); + } + } + outline.closePath(); + + return outline; + } + + public DifferentialVolumeEstimator(vtkPolyData polyData) { + this.globalPolyData = polyData; + this.referencePolyData = null; + this.referencePoints = null; + this.numProfiles = 0; + this.radialUp = false; + } + + /** + * Get the height of the shape model above the reference plane + * + * @param x in native coordinates + * @param y in native coordinates + * @return height, or {@link Double#NaN} if no intersection found + */ + public double getHeight(double x, double y) { + + double height = Double.NaN; + double[] origin = {x, y, 0}; + double[] direction = {0, 0, 1}; + double[] intersect = new double[3]; + + long cellID = nativeSBM.computeRayIntersection(origin, direction, intersect); + if (cellID < 0) { + direction[2] = -1; + cellID = nativeSBM.computeRayIntersection(origin, direction, intersect); + } + if (cellID >= 0) height = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect)); + + if (Double.isNaN(height)) return Double.NaN; + + return height; + } + + /** + * Get the height of the reference surface above the reference plane. + * + * @param x in native coordinates + * @param y in native coordinates + * @return height of surface at (x,y) above reference plane + */ + public double getRefHeight(double x, double y) { + + double[] origin = {x, y, 0}; + double[] direction = {0, 0, 1}; + double[] intersect; + + double refHeight = Double.NaN; + if (referenceSBM != null) { + intersect = new double[3]; + + long cellID = referenceSBM.computeRayIntersection(origin, direction, intersect); + if (cellID < 0) { + direction[2] = -1; + cellID = referenceSBM.computeRayIntersection(origin, direction, intersect); + } + if (cellID >= 0) { + refHeight = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect)); + } + } else { + refHeight = referenceSurface.value(x, y); + } + + if (Double.isNaN(refHeight)) return Double.NaN; + + return refHeight; + } + + /** + * Create an array of grid points with heights + * + * @param gridHalfExtent half-size of grid + * @param gridSpacing spacing between points + * @return grid points sorted by x coordinate, then y + */ + private NavigableSet createGrid(double gridHalfExtent, double gridSpacing) { + Set localGrid = new HashSet<>(); + + for (double x = 0; x <= gridHalfExtent; x += gridSpacing) { + for (double y = 0; y <= gridHalfExtent; y += gridSpacing) { + localGrid.add(new Vector3D(x, y, 0)); + if (y != 0) localGrid.add(new Vector3D(x, -y, 0)); + if (x != 0) localGrid.add(new Vector3D(-x, y, 0)); + if (x != 0 && y != 0) localGrid.add(new Vector3D(-x, -y, 0)); + } + } + this.gridSpacing = gridSpacing; + this.gridHalfExtent = gridHalfExtent; + + GridPoint highGridPoint = null; + GridPoint lowGridPoint = null; + NavigableSet gridPoints = new TreeSet<>(); + for (Vector3D localPoint : localGrid) { + GridPoint gp = new GridPoint(localPoint); + gridPoints.add(gp); + if (Double.isFinite(gp.height)) { + if (highGridPoint == null || highGridPoint.height < gp.height) highGridPoint = gp; + if (lowGridPoint == null || lowGridPoint.height > gp.height) lowGridPoint = gp; + } + } + + if (highGridPoint != null) highPoint = highGridPoint.globalIJK; + if (lowGridPoint != null) lowPoint = lowGridPoint.globalIJK; + + return gridPoints; + } + + /** + * Create the reference surface, either from a fit to a set of points or from an input shape + * model. + * + * @param localOriginInGlobalCoordinates local origin in global coordinates + */ + public void createReference(Vector3D localOriginInGlobalCoordinates) { - vtkPolyData polydata = PolyDataUtil.loadShapeModel(filename); double[] pt = new double[3]; - for (int i = 0; i < polydata.GetNumberOfPoints(); i++) { - polydata.GetPoint(i, pt); - points.add(new Vector3D(pt)); + + if (referencePolyData != null) { + if (referencePoints != null) { + logger.warn( + "Both -referenceList and -referenceShape were specified. Reference surface will be set to argument of -referenceShape."); + } + referencePoints = new ArrayList<>(); + for (int i = 0; i < referencePolyData.GetNumberOfPoints(); i++) { + referencePolyData.GetPoint(i, pt); + referencePoints.add(new Vector3D(pt)); + } } - } else { - List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); - for (String line : lines) { - if (line.trim().isEmpty() || line.trim().startsWith("#")) - continue; - SBMTStructure structure = SBMTStructure.fromString(line); - points.add(structure.centerXYZ()); + + // this is the best fit plane to the reference points. It can convert points in input + // (global) coordinates to native coordinates and vice versa + plane = new FitPlane(referencePoints); + + // set the +Z direction for the local plane + Vector3D referenceNormal = radialUp ? plane.getTransform().getValue() : Vector3D.PLUS_K; + + // check if the plane normal is pointing in the same direction as the reference normal. If not, + // flip the plane + Pair transform = plane.getTransform(); + Vector3D planeNormal = transform.getKey().applyInverseTo(referenceNormal); + if (planeNormal.dotProduct(referenceNormal) < 0) plane = plane.reverseNormal(); + + // create the SmallBodyModel for the shape to evaluate + vtkPolyData nativePolyData = new vtkPolyData(); + nativePolyData.DeepCopy(globalPolyData); + vtkPoints points = nativePolyData.GetPoints(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + points.GetPoint(i, pt); + Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt)); + double[] data = nativePoint.toArray(); + points.SetPoint(i, data); } - } - } catch (Exception e) { - logger.warn(e.getLocalizedMessage()); - } - return points; - } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("gridExtent").required().hasArg().desc( - "Required. Size of local grid, in same units as shape model and reference surface. Grid is assumed to be square.") - .build()); - options.addOption(Option.builder("gridSpacing").required().hasArg() - .desc( - "Required. Spacing of local grid, in same units as shape model and reference surface.") - .build()); - options.addOption(Option.builder("logFile").hasArg() - .desc("If present, save screen output to log file.").build()); - options.addOption(Option.builder("logLevel").hasArg() - .desc("If present, print messages above selected priority. Valid values are " - + "ALL, OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, or FINEST. Default is INFO.") - .build()); - options.addOption(Option.builder("numProfiles").hasArg().desc( - "Number of radial profiles to create. Profiles are evenly spaced in degrees and evaluated " - + "at intervals of gridSpacing in the radial direction.") - .build()); - options.addOption(Option.builder("origin").hasArg() - .desc("If present, set origin of local coordinate system. " - + "Options are MAX_HEIGHT (set to maximum elevation of the shape model), " - + "MIN_HEIGHT (set to minimum elevation of the shape model), " - + "or a three element vector specifying the desired origin, comma separated, no spaces (e.g. 11.45,-45.34,0.932).") - .build()); - options.addOption(Option.builder("output").hasArg().required().desc( - "Basename of output files. Files will be named ${output}_grid.csv for the grid, ${output}_sector.csv for the sectors, " - + "and ${output}_profile_${degrees}.csv for profiles.") - .build()); - options.addOption(Option.builder("radialUp") - .desc("Specify +Z direction of local coordinate system to be in the radial " - + "direction. Default is to align local +Z along global +Z.") - .build()); - options.addOption(Option.builder("referenceList").hasArg().desc( - "File containing reference points. If the file extension is .vtk it is read as a VTK file, " - + "otherwise it is assumed to be an SBMT structure file.") - .build()); - options.addOption(Option.builder("referenceShape").hasArg().desc("Reference shape.").build()); - options.addOption(Option.builder("referenceVTK").hasArg() - .desc("If present, write out a VTK file with the reference surface at each grid point. " - + "If an ROI is defined color points inside/outside the boundaries.") - .build()); - options.addOption(Option.builder("roiInner").hasArg().desc( - "Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.") - .build()); - options.addOption(Option.builder("roiOuter").hasArg().desc( - "Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.") - .build()); - options.addOption(Option.builder("shapeModel").hasArg().required() - .desc("Shape model for volume computation.").build()); return options; - } + nativeSBM = new SmallBodyModel(nativePolyData); + // now define the reference shape/surface + if (referencePolyData != null) { + // create the SmallBodyModel for the reference shape + nativePolyData = new vtkPolyData(); + nativePolyData.DeepCopy(referencePolyData); + points = nativePolyData.GetPoints(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + points.GetPoint(i, pt); + Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt)); + double[] data = nativePoint.toArray(); + points.SetPoint(i, data); + } - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new DifferentialVolumeEstimator(); + referenceSBM = new SmallBodyModel(nativePolyData); + } else { + // create the reference surface + List nativePoints = new ArrayList<>(); + for (Vector3D v : referencePoints) { + Vector3D nativePoint = plane.globalToLocal(v); + nativePoints.add(nativePoint); + } - Options options = defineOptions(); + referenceSurface = new FitSurface(nativePoints, POLYNOMIAL_DEGREE); + } - CommandLine cl = defaultOBJ.parseArgs(args, options); + // create a rotation matrix to go from native to local (where the Z axis is the same for both + // and the X axis is aligned in the same direction as the global X axis) + Pair globalToLocalTransform = plane.getTransform(); + Vector3D kRow = Vector3D.PLUS_K; + Vector3D iRow = globalToLocalTransform.getKey().applyTo(Vector3D.PLUS_I); + Vector3D jRow = Vector3D.crossProduct(kRow, iRow).normalize(); + kRow = Vector3D.crossProduct(iRow, jRow).normalize(); + iRow = iRow.normalize(); - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + Vector3D translateNativeToLocal = Vector3D.ZERO; + if (localOriginInGlobalCoordinates.getNorm() > 0) { + // translation to go from native to local (where localOriginInGlobalCoordinates defines 0,0 in + // the local frame) + Vector3D nativeOriginInGlobalCoordinates = plane.localToGlobal(Vector3D.ZERO); + Vector3D translateNativeToLocalInGlobalCoordinates = + localOriginInGlobalCoordinates.subtract(nativeOriginInGlobalCoordinates); + // TODO: check that the Z component is zero (it should be?) + translateNativeToLocal = globalToLocalTransform.getKey().applyTo(translateNativeToLocalInGlobalCoordinates); + } - StringBuilder header = new StringBuilder(); - header.append("# ").append(new Date()).append("\n"); - header.append("# ").append(defaultOBJ.getClass().getSimpleName()).append(" [").append(AppVersion.getVersionString()).append("]\n"); - header.append("# ").append(startupMessages.get(MessageLabel.ARGUMENTS)).append("\n"); + Rotation rotateNativeToLocal = MathConversions.toRotation(new RotationMatrixIJK( + iRow.getX(), + jRow.getX(), + kRow.getX(), + iRow.getY(), + jRow.getY(), + kRow.getY(), + iRow.getZ(), + jRow.getZ(), + kRow.getZ())); - NativeLibraryLoader.loadVtkLibraries(); - - double gridHalfExtent = Double.parseDouble(cl.getOptionValue("gridExtent")) / 2; - double gridSpacing = Double.parseDouble(cl.getOptionValue("gridSpacing")); - - String outputBasename = cl.getOptionValue("output"); - String dirName = FilenameUtils.getFullPath(outputBasename); - if (!dirName.trim().isEmpty()) { - File dir = new File(dirName); - if (!dir.exists()) - dir.mkdirs(); + this.nativeToLocal = new AbstractMap.SimpleEntry<>(rotateNativeToLocal, translateNativeToLocal); } - vtkPolyData polyData = null; - try { - polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("shapeModel")); - } catch (Exception e) { - logger.error("Cannot load shape model!"); - logger.error(e.getLocalizedMessage(), e); - } - - ORIGIN originType = ORIGIN.DEFAULT; - Vector3D localOrigin = Vector3D.ZERO; - if (cl.hasOption("origin")) { - String originString = cl.getOptionValue("origin"); - if (originString.contains(",")) { - String[] parts = originString.split(","); - if (parts.length == 3) { - localOrigin = new Vector3D(Double.parseDouble(parts[0].trim()), - Double.parseDouble(parts[1].trim()), Double.parseDouble(parts[2].trim())); - originType = ORIGIN.CUSTOM; - } - } else { - originType = ORIGIN.valueOf(originString.toUpperCase()); - } + /** + * The header for grid and profile CSV files. Each line begins with a # + * + * @param header string at beginning of header + * @return complete header + */ + public static String getHeader(String header) { + StringBuffer sb = new StringBuffer(); + sb.append(header); + sb.append("# Local X and Y are grid coordinates in the local reference frame\n"); + sb.append("# Angle is measured from the local X axis, in degrees\n"); + sb.append("# ROI flag is 1 if point is in the region of interest, 0 if not\n"); + sb.append("# Global X, Y, and Z are the local grid points in the global " + " (input) reference system\n"); + sb.append("# Reference Height is the height of the reference model (or fit surface) above " + + "the local grid plane\n"); + sb.append("# Model Height is the height of the shape model above the local grid plane. " + + "NaN means there is no model intersection at this grid point.\n"); + sb.append("# Bin volume is the grid cell area times the model - reference height\n"); + sb.append("#\n"); + sb.append(String.format("%s, ", "Local X")); + sb.append(String.format("%s, ", "Local Y")); + sb.append(String.format("%s, ", "Angle")); + sb.append(String.format("%s, ", "ROI Flag")); + sb.append(String.format("%s, ", "Global X")); + sb.append(String.format("%s, ", "Global Y")); + sb.append(String.format("%s, ", "Global Z")); + sb.append(String.format("%s, ", "Reference Height")); + sb.append(String.format("%s, ", "Model Height")); + sb.append(String.format("%s, ", "Model - Reference")); + sb.append(String.format("%s", "Bin Volume")); + return sb.toString(); } - DifferentialVolumeEstimator app = new DifferentialVolumeEstimator(polyData); - if (cl.hasOption("numProfiles")) - app.setNumProfiles(Integer.parseInt(cl.getOptionValue("numProfiles"))); + /** + * The header for the sector CSV file. Each line begins with a # + * + * @param header string at beginning of header + * @return complete header + */ + public static String getSectorHeader(String header) { + StringBuilder sb = new StringBuilder(); + sb.append(header); + sb.append("# Angle is measured from the local X axis, in degrees\n"); + sb.append( + "# Sector volume is the grid cell area times the model - reference height summed over all grid cells in the ROI.\n"); + sb.append("#\n"); + sb.append(String.format("%s, ", "Index")); + sb.append(String.format("%s, ", "Start angle (degrees)")); + sb.append(String.format("%s, ", "Stop angle (degrees)")); + sb.append(String.format("%s, ", "Sector Volume above reference surface")); + sb.append(String.format("%s, ", "Sector Volume below reference surface")); + sb.append(String.format("%s", "Total Sector Volume")); - if (cl.hasOption("radialUp")) - app.setRadialUp(true); + return sb.toString(); + } + + private String toCSV(GridPoint gp) { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("%f, ", gp.localIJK.getX())); + sb.append(String.format("%f, ", gp.localIJK.getY())); + + double angle = Math.toDegrees(Math.atan2(gp.localIJK.getY(), gp.localIJK.getX())); + if (angle < 0) angle += 360; + sb.append(String.format("%f, ", angle)); + sb.append(String.format("%d, ", isInsideROI(gp.localIJK) ? 1 : 0)); + + sb.append(String.format("%f, ", gp.globalIJK.getX())); + sb.append(String.format("%f, ", gp.globalIJK.getY())); + sb.append(String.format("%f, ", gp.globalIJK.getZ())); + + sb.append(String.format("%g, ", gp.referenceHeight)); + sb.append(String.format("%g, ", gp.height)); + sb.append(String.format("%g, ", gp.differentialHeight)); + sb.append(String.format("%g", gridSpacing * gridSpacing * gp.differentialHeight)); + + return sb.toString(); + } + + /** + * + * @param localIJ Point on the local grid. The Z coordinate is ignored + * @return true if the point is inside the outer boundary and outside the inner boundary. If the + * outer boundary is null then all points are considered to be inside the outer boundary. If the + * inner boundary is null all points are considered to be outside the inner boundary. + */ + private boolean isInsideROI(Vector3D localIJ) { + Point2D thisPoint = new Point2D.Double(localIJ.getX(), localIJ.getY()); + boolean insideROI = roiOuter == null || roiOuter.contains(thisPoint); + if (roiInner != null) { + if (insideROI && roiInner.contains(thisPoint)) insideROI = false; + } + return insideROI; + } + + /** + * Write out a VTK file with the local grid points. Useful for a sanity check. + * + * @param gridPointsList grid points + * @param profilesMap grid points along each profile + * @param sectorsMap grid points within each sector + * @param vtkFile file to write + */ + private void writeReferenceVTK( + Collection gridPointsList, + Map> profilesMap, + Map> sectorsMap, + String vtkFile) { + + Map roiMap = new HashMap<>(); + Map profileMap = new HashMap<>(); + Map sectorMap = new HashMap<>(); + + for (GridPoint gp : gridPointsList) { + Vector3D localIJK = gp.localIJK; + Vector3D nativeIJK = nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); + Vector3D globalIJK = plane.localToGlobal(nativeIJK); + roiMap.put(globalIJK, isInsideROI(gp.localIJK)); + profileMap.put(globalIJK, 0); + sectorMap.put(globalIJK, 0); + } + + for (int i : profilesMap.keySet()) { + for (GridPoint gp : profilesMap.get(i)) { + Vector3D localIJK = gp.localIJK; + Vector3D nativeIJK = + nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); + Vector3D globalIJK = plane.localToGlobal(nativeIJK); + profileMap.put(globalIJK, i + 1); + } + } + + for (int i : sectorsMap.keySet()) { + for (GridPoint gp : sectorsMap.get(i)) { + Vector3D localIJK = gp.localIJK; + Vector3D nativeIJK = + nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); + Vector3D globalIJK = plane.localToGlobal(nativeIJK); + sectorMap.put(globalIJK, i + 1); + } + } + + vtkDoubleArray insideROI = new vtkDoubleArray(); + insideROI.SetName("Inside ROI"); + + vtkDoubleArray profiles = new vtkDoubleArray(); + profiles.SetName("Profiles"); + + vtkDoubleArray sectors = new vtkDoubleArray(); + sectors.SetName("Sectors"); + + vtkPoints pointsXYZ = new vtkPoints(); + for (Vector3D point : roiMap.keySet()) { + double[] array = point.toArray(); + pointsXYZ.InsertNextPoint(array); + insideROI.InsertNextValue(roiMap.get(point) ? 1 : 0); + profiles.InsertNextValue(profileMap.get(point)); + sectors.InsertNextValue(sectorMap.get(point)); + } + + vtkPolyData polyData = new vtkPolyData(); + polyData.SetPoints(pointsXYZ); + polyData.GetPointData().AddArray(insideROI); + polyData.GetPointData().AddArray(profiles); + polyData.GetPointData().AddArray(sectors); + + vtkCellArray cells = new vtkCellArray(); + polyData.SetPolys(cells); + + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + vtkIdList idList = new vtkIdList(); + idList.InsertNextId(i); + cells.InsertNextCell(idList); + } + + vtkPolyDataWriter writer = new vtkPolyDataWriter(); + writer.SetInputData(polyData); + writer.SetFileName(vtkFile); + writer.SetFileTypeToBinary(); + writer.Update(); + } + + /** + * Write the local grid out to a file + * + * @param gridPoints grid points + * @param header file header + * @param outputBasename CSV file to write + */ + private void writeGridCSV(Collection gridPoints, String header, String outputBasename) { + String csvFile = outputBasename + "_grid.csv"; + + try (PrintWriter pw = new PrintWriter(csvFile)) { + pw.println(getHeader(header)); + for (GridPoint gp : gridPoints) pw.println(toCSV(gp)); + } catch (FileNotFoundException e) { + logger.warn("Can't write " + csvFile); + logger.warn(e.getLocalizedMessage()); + } + } + + /** + * Write profiles to file + * + * @param gridPoints grid points + * @param header file header + * @param outputBasename CSV file to write + */ + private Map> writeProfileCSV( + Collection gridPoints, String header, String outputBasename) { + Map> profileMap = new HashMap<>(); + + if (numProfiles == 0) return profileMap; + + // sort grid points into radial bins + NavigableMap> radialMap = new TreeMap<>(); + for (GridPoint gp : gridPoints) { + double radius = gp.localIJK.getNorm() / gridSpacing; + int key = (int) radius; + Set set = radialMap.computeIfAbsent(key, k -> new HashSet<>()); + set.add(gp); + } + + final double deltaAngle = 2 * Math.PI / numProfiles; + for (int i = 0; i < numProfiles; i++) { + + Collection profileGridPoints = new HashSet<>(); + profileMap.put(i, profileGridPoints); + + double angle = deltaAngle * i; + String csvFile = + String.format("%s_profile_%03d.csv", outputBasename, (int) Math.round(Math.toDegrees(angle))); + + try (PrintWriter pw = new PrintWriter(csvFile)) { + pw.println(getHeader(header)); + for (int bin : radialMap.keySet()) { + + // stop profile at grid edge + double thisX = Math.abs(Math.cos(angle) * bin) * gridSpacing; + if (thisX > gridHalfExtent) continue; + double thisY = Math.abs(Math.sin(angle) * bin) * gridSpacing; + if (thisY > gridHalfExtent) continue; + + // sort points in this radial bin by angular distance from profile angle + NavigableSet sortedByAngle = new TreeSet<>((o1, o2) -> { + double angle1 = Math.atan2(o1.localIJK.getY(), o1.localIJK.getX()); + if (angle1 < 0) angle1 += 2 * Math.PI; + double angle2 = Math.atan2(o2.localIJK.getY(), o2.localIJK.getX()); + if (angle2 < 0) angle2 += 2 * Math.PI; + return Double.compare(Math.abs(angle1 - angle), Math.abs(angle2 - angle)); + }); + + sortedByAngle.addAll(radialMap.get(bin)); + + pw.println(toCSV(sortedByAngle.first())); + GridPoint thisPoint = sortedByAngle.first(); + if (Double.isFinite(thisPoint.differentialHeight)) profileGridPoints.add(thisPoint); + } + } catch (FileNotFoundException e) { + logger.warn("Can't write {}", csvFile); + logger.warn(e.getLocalizedMessage()); + } + } + + return profileMap; + } + + /** + * Write sector volumes to a file + * + * @param gridPoints grid points + * @param header file header + * @param outputBasename CSV file to write + */ + private Map> writeSectorCSV( + Collection gridPoints, String header, String outputBasename) { + + // grid points in each sector + Map> sectorMap = new HashMap<>(); + + if (numProfiles == 0) return sectorMap; + + String csvFile = outputBasename + "_sector.csv"; + + NavigableMap aboveMap = new TreeMap<>(); + NavigableMap belowMap = new TreeMap<>(); + final double deltaAngle = 2 * Math.PI / numProfiles; + for (int i = 0; i < numProfiles; i++) { + aboveMap.put(i * deltaAngle, 0.); + belowMap.put(i * deltaAngle, 0.); + } + + // run through all the grid points and put them in the appropriate sector + double gridCellArea = gridSpacing * gridSpacing; + for (GridPoint gp : gridPoints) { + Vector3D localIJK = gp.localIJK; + double azimuth = Math.atan2(localIJK.getY(), localIJK.getX()); + if (azimuth < 0) azimuth += 2 * Math.PI; + double key = aboveMap.floorKey(azimuth); + + int sector = (int) (key / deltaAngle); + Collection sectorGridPoints = sectorMap.computeIfAbsent(sector, k -> new HashSet<>()); + + if (isInsideROI(gp.localIJK)) { + double dv = gridCellArea * gp.differentialHeight; + if (Double.isFinite(dv)) { + if (dv > 0) { + aboveMap.compute(key, (k, value) -> value + dv); + } else { + belowMap.compute(key, (k, value) -> value + dv); + } + sectorGridPoints.add(gp); + } + } + } + + try (PrintWriter pw = new PrintWriter(csvFile)) { + pw.println(getSectorHeader(header)); + for (double azimuth : aboveMap.keySet()) { + StringBuffer sb = new StringBuffer(); + sb.append(String.format("%d, ", (int) (azimuth / deltaAngle))); + sb.append(String.format("%.2f, ", Math.toDegrees(azimuth))); + sb.append(String.format("%.2f, ", Math.toDegrees(azimuth + deltaAngle))); + sb.append(String.format("%e, ", aboveMap.get(azimuth))); + sb.append(String.format("%e, ", belowMap.get(azimuth))); + sb.append(String.format("%e", aboveMap.get(azimuth) + belowMap.get(azimuth))); + pw.println(sb); + } + + } catch (FileNotFoundException e) { + logger.warn("Can't write " + csvFile); + logger.warn(e.getLocalizedMessage()); + } + + return sectorMap; + } + + private class GridPoint implements Comparable { + Vector3D localIJK; + Vector3D globalIJK; + double referenceHeight; + double height; + double differentialHeight; + + /** + * Create a grid point from an input location in local coordinates + * + * @param xy point in local coordinates. Z value is ignored. + */ + public GridPoint(Vector3D xy) { + this.localIJK = xy; + Vector3D nativeIJK = nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue()); + globalIJK = plane.localToGlobal(nativeIJK); + referenceHeight = getRefHeight(nativeIJK.getX(), nativeIJK.getY()); + height = getHeight(nativeIJK.getX(), nativeIJK.getY()); + differentialHeight = height - referenceHeight; + } + + /** + * sort by the x coordinate on the local grid, then by the y coordinate. + */ + @Override + public int compareTo(GridPoint o) { + int compare = Double.compare(localIJK.getX(), o.localIJK.getX()); + if (compare == 0) compare = Double.compare(localIJK.getY(), o.localIJK.getY()); + return compare; + } + } + + private static List readPointsFromFile(String filename) { + List points = new ArrayList<>(); - if (cl.hasOption("referenceShape")) { try { - app.setReferencePolyData(PolyDataUtil.loadShapeModel(cl.getOptionValue("referenceShape"))); + if (FilenameUtils.getExtension(filename).equalsIgnoreCase("vtk")) { + + vtkPolyData polydata = PolyDataUtil.loadShapeModel(filename); + double[] pt = new double[3]; + for (int i = 0; i < polydata.GetNumberOfPoints(); i++) { + polydata.GetPoint(i, pt); + points.add(new Vector3D(pt)); + } + } else { + List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); + for (String line : lines) { + if (line.trim().isEmpty() || line.trim().startsWith("#")) continue; + SBMTStructure structure = SBMTStructure.fromString(line); + points.add(structure.centerXYZ()); + } + } } catch (Exception e) { - logger.error("Cannot load reference shape model!"); + logger.warn(e.getLocalizedMessage()); + } + return points; + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("gridExtent") + .required() + .hasArg() + .desc( + "Required. Size of local grid, in same units as shape model and reference surface. Grid is assumed to be square.") + .build()); + options.addOption(Option.builder("gridSpacing") + .required() + .hasArg() + .desc("Required. Spacing of local grid, in same units as shape model and reference surface.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + "ALL, OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, or FINEST. Default is INFO.") + .build()); + options.addOption(Option.builder("numProfiles") + .hasArg() + .desc("Number of radial profiles to create. Profiles are evenly spaced in degrees and evaluated " + + "at intervals of gridSpacing in the radial direction.") + .build()); + options.addOption(Option.builder("origin") + .hasArg() + .desc( + "If present, set origin of local coordinate system. " + + "Options are MAX_HEIGHT (set to maximum elevation of the shape model), " + + "MIN_HEIGHT (set to minimum elevation of the shape model), " + + "or a three element vector specifying the desired origin, comma separated, no spaces (e.g. 11.45,-45.34,0.932).") + .build()); + options.addOption(Option.builder("output") + .hasArg() + .required() + .desc( + "Basename of output files. Files will be named ${output}_grid.csv for the grid, ${output}_sector.csv for the sectors, " + + "and ${output}_profile_${degrees}.csv for profiles.") + .build()); + options.addOption(Option.builder("radialUp") + .desc("Specify +Z direction of local coordinate system to be in the radial " + + "direction. Default is to align local +Z along global +Z.") + .build()); + options.addOption(Option.builder("referenceList") + .hasArg() + .desc("File containing reference points. If the file extension is .vtk it is read as a VTK file, " + + "otherwise it is assumed to be an SBMT structure file.") + .build()); + options.addOption(Option.builder("referenceShape") + .hasArg() + .desc("Reference shape.") + .build()); + options.addOption(Option.builder("referenceVTK") + .hasArg() + .desc("If present, write out a VTK file with the reference surface at each grid point. " + + "If an ROI is defined color points inside/outside the boundaries.") + .build()); + options.addOption(Option.builder("roiInner") + .hasArg() + .desc( + "Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.") + .build()); + options.addOption(Option.builder("roiOuter") + .hasArg() + .desc( + "Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.") + .build()); + options.addOption(Option.builder("shapeModel") + .hasArg() + .required() + .desc("Shape model for volume computation.") + .build()); + return options; + } + + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new DifferentialVolumeEstimator(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + StringBuilder header = new StringBuilder(); + header.append("# ").append(new Date()).append("\n"); + header.append("# ") + .append(defaultOBJ.getClass().getSimpleName()) + .append(" [") + .append(AppVersion.getVersionString()) + .append("]\n"); + header.append("# ").append(startupMessages.get(MessageLabel.ARGUMENTS)).append("\n"); + + NativeLibraryLoader.loadVtkLibraries(); + + double gridHalfExtent = Double.parseDouble(cl.getOptionValue("gridExtent")) / 2; + double gridSpacing = Double.parseDouble(cl.getOptionValue("gridSpacing")); + + String outputBasename = cl.getOptionValue("output"); + String dirName = FilenameUtils.getFullPath(outputBasename); + if (!dirName.trim().isEmpty()) { + File dir = new File(dirName); + if (!dir.exists()) dir.mkdirs(); + } + + vtkPolyData polyData = null; + try { + polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("shapeModel")); + } catch (Exception e) { + logger.error("Cannot load shape model!"); logger.error(e.getLocalizedMessage(), e); } - } - if (cl.hasOption("referenceList")) - app.setReferencePoints(readPointsFromFile(cl.getOptionValue("referenceList"))); - app.createReference(localOrigin); + ORIGIN originType = ORIGIN.DEFAULT; + Vector3D localOrigin = Vector3D.ZERO; + if (cl.hasOption("origin")) { + String originString = cl.getOptionValue("origin"); + if (originString.contains(",")) { + String[] parts = originString.split(","); + if (parts.length == 3) { + localOrigin = new Vector3D( + Double.parseDouble(parts[0].trim()), + Double.parseDouble(parts[1].trim()), + Double.parseDouble(parts[2].trim())); + originType = ORIGIN.CUSTOM; + } + } else { + originType = ORIGIN.valueOf(originString.toUpperCase()); + } + } + DifferentialVolumeEstimator app = new DifferentialVolumeEstimator(polyData); + + if (cl.hasOption("numProfiles")) app.setNumProfiles(Integer.parseInt(cl.getOptionValue("numProfiles"))); + + if (cl.hasOption("radialUp")) app.setRadialUp(true); + + if (cl.hasOption("referenceShape")) { + try { + app.setReferencePolyData(PolyDataUtil.loadShapeModel(cl.getOptionValue("referenceShape"))); + } catch (Exception e) { + logger.error("Cannot load reference shape model!"); + logger.error(e.getLocalizedMessage(), e); + } + } + if (cl.hasOption("referenceList")) + app.setReferencePoints(readPointsFromFile(cl.getOptionValue("referenceList"))); - // Shift the origin if needed - switch (originType) { - case CUSTOM: - case DEFAULT: - break; - case MAX_HEIGHT: - app.createGrid(gridHalfExtent, 0.1 * gridSpacing); - localOrigin = app.highPoint; app.createReference(localOrigin); - break; - case MIN_HEIGHT: - app.createGrid(gridHalfExtent, 0.1 * gridSpacing); - localOrigin = app.lowPoint; - app.createReference(localOrigin); - break; + + // Shift the origin if needed + switch (originType) { + case CUSTOM: + case DEFAULT: + break; + case MAX_HEIGHT: + app.createGrid(gridHalfExtent, 0.1 * gridSpacing); + localOrigin = app.highPoint; + app.createReference(localOrigin); + break; + case MIN_HEIGHT: + app.createGrid(gridHalfExtent, 0.1 * gridSpacing); + localOrigin = app.lowPoint; + app.createReference(localOrigin); + break; + } + + if (cl.hasOption("roiInner")) app.setInnerROI(cl.getOptionValue("roiInner")); + + if (cl.hasOption("roiOuter")) app.setOuterROI(cl.getOptionValue("roiOuter")); + + NavigableSet gridPoints = app.createGrid(gridHalfExtent, gridSpacing); + + app.writeGridCSV(gridPoints, header.toString(), outputBasename); + Map> profileMap = + app.writeProfileCSV(gridPoints, header.toString(), outputBasename); + Map> sectorMap = + app.writeSectorCSV(gridPoints, header.toString(), outputBasename); + + if (cl.hasOption("referenceVTK")) { + app.writeReferenceVTK(gridPoints, profileMap, sectorMap, cl.getOptionValue("referenceVTK")); + } + + logger.info("Finished."); } - - if (cl.hasOption("roiInner")) - app.setInnerROI(cl.getOptionValue("roiInner")); - - if (cl.hasOption("roiOuter")) - app.setOuterROI(cl.getOptionValue("roiOuter")); - - NavigableSet gridPoints = app.createGrid(gridHalfExtent, gridSpacing); - - app.writeGridCSV(gridPoints, header.toString(), outputBasename); - Map> profileMap = - app.writeProfileCSV(gridPoints, header.toString(), outputBasename); - Map> sectorMap = - app.writeSectorCSV(gridPoints, header.toString(), outputBasename); - - if (cl.hasOption("referenceVTK")) { - app.writeReferenceVTK(gridPoints, profileMap, sectorMap, cl.getOptionValue("referenceVTK")); - } - - logger.info("Finished."); - } } diff --git a/src/main/java/terrasaur/apps/DumpConfig.java b/src/main/java/terrasaur/apps/DumpConfig.java index c340cc6..a47f66c 100644 --- a/src/main/java/terrasaur/apps/DumpConfig.java +++ b/src/main/java/terrasaur/apps/DumpConfig.java @@ -43,56 +43,54 @@ import terrasaur.utils.AppVersion; public class DumpConfig implements TerrasaurTool { - @Override - public String shortDescription() { - return "Write a sample configuration file to use with Terrasaur, using defaults for DART."; - } - - @Override - public String fullDescription(Options options) { - return WordUtils.wrap( - "This program writes out sample configuration files to be used with Terrasaur. " - + "It takes a single argument, which is the name of the directory that will contain " - + "the configuration files to be written.", - 80); - } - - public static void main(String[] args) { - // if no arguments, print the usage and exit - if (args.length == 0) { - System.out.println(new DumpConfig().fullDescription(null)); - System.exit(0); + @Override + public String shortDescription() { + return "Write a sample configuration file to use with Terrasaur, using defaults for DART."; } - // if -shortDescription is specified, print short description and exit. - for (String arg : args) { - if (arg.equals("-shortDescription")) { - System.out.println(new DumpConfig().shortDescription()); - System.exit(0); - } + @Override + public String fullDescription(Options options) { + return WordUtils.wrap( + "This program writes out sample configuration files to be used with Terrasaur. " + + "It takes a single argument, which is the name of the directory that will contain " + + "the configuration files to be written.", + 80); } - File path = Paths.get(args[0]).toFile(); + public static void main(String[] args) { + // if no arguments, print the usage and exit + if (args.length == 0) { + System.out.println(new DumpConfig().fullDescription(null)); + System.exit(0); + } - ConfigBlock configBlock = TerrasaurConfig.getTemplate(); - try (PrintWriter pw = new PrintWriter(path)) { - PropertiesConfiguration config = new ConfigBlockFactory().toConfig(configBlock); - PropertiesConfigurationLayout layout = config.getLayout(); + // if -shortDescription is specified, print short description and exit. + for (String arg : args) { + if (arg.equals("-shortDescription")) { + System.out.println(new DumpConfig().shortDescription()); + System.exit(0); + } + } - String now = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withLocale(Locale.getDefault()) - .withZone(ZoneOffset.UTC) - .format(Instant.now()); - layout.setHeaderComment( - String.format( - "Configuration file for %s\nCreated %s UTC", AppVersion.getVersionString(), now)); + File path = Paths.get(args[0]).toFile(); - config.write(pw); - } catch (ConfigurationException | IOException e) { - throw new RuntimeException(e); + ConfigBlock configBlock = TerrasaurConfig.getTemplate(); + try (PrintWriter pw = new PrintWriter(path)) { + PropertiesConfiguration config = new ConfigBlockFactory().toConfig(configBlock); + PropertiesConfigurationLayout layout = config.getLayout(); + + String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withLocale(Locale.getDefault()) + .withZone(ZoneOffset.UTC) + .format(Instant.now()); + layout.setHeaderComment( + String.format("Configuration file for %s\nCreated %s UTC", AppVersion.getVersionString(), now)); + + config.write(pw); + } catch (ConfigurationException | IOException e) { + throw new RuntimeException(e); + } + + System.out.println("Wrote config file to " + path.getAbsolutePath()); } - - System.out.println("Wrote config file to " + path.getAbsolutePath()); - } } diff --git a/src/main/java/terrasaur/apps/FacetInfo.java b/src/main/java/terrasaur/apps/FacetInfo.java index b4d141f..9dbca0e 100644 --- a/src/main/java/terrasaur/apps/FacetInfo.java +++ b/src/main/java/terrasaur/apps/FacetInfo.java @@ -46,213 +46,216 @@ import vtk.vtkPolyData; public class FacetInfo implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + 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() {} + /** + * 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 shortDescription() { + return "Print info about a facet."; + } - @Override - public String fullDescription(Options options) { - String header = "Prints information about facet(s)."; - String footer = - """ + @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 neighbors(long cellId) { - NavigableSet 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 TerrasaurTool.super.fullDescription(options, header, footer); } - return neighborCellIds; - } + private vtkPolyData polyData; + private vtkOBBTree searchTree; + private Vector3D origin; - @Value.Immutable - public abstract static class FacetInfoLine { + private FacetInfo(vtkPolyData polyData) { + this.polyData = polyData; + PolyDataStatistics stats = new PolyDataStatistics(polyData); + origin = new Vector3D(stats.getCentroid()); - public abstract long index(); + logger.info("Origin is at {}", origin); - public abstract Vector3D radius(); - - public abstract Vector3D normal(); + logger.info("Creating search tree"); + searchTree = new vtkOBBTree(); + searchTree.SetDataSet(polyData); + searchTree.SetTolerance(1e-12); + searchTree.BuildLocator(); + } /** - * @return facets between this and origin + * @param cellId id of this cell + * @return Set of neighboring cells (ones which share a vertex with this one) */ - public abstract NavigableSet interiorIntersections(); + private NavigableSet neighbors(long cellId) { + NavigableSet neighborCellIds = new TreeSet<>(); - /** - * @return facets between this and infinity - */ - public abstract NavigableSet exteriorIntersections(); + vtkIdList vertexIdlist = new vtkIdList(); + CellInfo.getCellInfo(polyData, cellId, vertexIdlist); - 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 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 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 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 neighbors = app.neighbors(cellId); - for (long neighborCellId : neighbors) - pw.println(app.getFacetInfoLine(neighborCellId).toCSV()); + vtkIdList facetIdlist = new vtkIdList(); + for (long i = 0; i < vertexIdlist.GetNumberOfIds(); i++) { + long vertexId = vertexIdlist.GetId(i); + polyData.GetPointCells(vertexId, facetIdlist); } - } - logger.info("Wrote {}", cl.getOptionValue("output")); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); + for (long i = 0; i < facetIdlist.GetNumberOfIds(); i++) { + long id = facetIdlist.GetId(i); + if (id == cellId) continue; + neighborCellIds.add(id); + } + + return neighborCellIds; } - logger.info("Finished."); - } + @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 interiorIntersections(); + + /** + * @return facets between this and infinity + */ + public abstract NavigableSet 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 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 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 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 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."); + } } diff --git a/src/main/java/terrasaur/apps/GetSpots.java b/src/main/java/terrasaur/apps/GetSpots.java index 959345e..9cb672c 100644 --- a/src/main/java/terrasaur/apps/GetSpots.java +++ b/src/main/java/terrasaur/apps/GetSpots.java @@ -178,24 +178,24 @@ import vtk.vtkPolyData; */ public class GetSpots implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private GetSpots() {} + private GetSpots() {} - @Override - public String shortDescription() { - return "find relevant OSIRIS-REx data for assigning values to facets in an OBJ file."; - } + @Override + public String shortDescription() { + return "find relevant OSIRIS-REx data for assigning values to facets in an OBJ file."; + } - @Override - public String fullDescription(Options options) { - String header = - """ + @Override + public String fullDescription(Options options) { + String header = + """ This program identifies those times when the boresight of instrumenttype intersects the surface of Bennu less than a specified distance from the center of individual facets in shape model. """; - String footer = - """ + String footer = + """ All output is written to standard output in the following format: F1 sclkvalue dist pos frac flag inc ems phs [otherdata] @@ -225,424 +225,403 @@ public class GetSpots implements TerrasaurTool { phs is the phase angle in degrees [otherdata] is all textual data on the line following the sclk value, up to the linefeed, unchanged. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private String spicemetakernel; - private String objfile; - private String instrument; - private String sclkfile; - private int debugLevel; - private double maxdist; - private vtkPolyData polydata; - private SmallBodyModel smallBodyModel; - private HashMap coverageMap; - - private int SC_ID; - private String SC_ID_String; - private ReferenceFrame BodyFixed; - private String TARGET; - private final Vector3 NORTH = new Vector3(0, 0, 1e6); - private int instID; - - private PrintStream outputStream; - - public GetSpots( - String spicemetakernel, - String objfile, - String instrument, - String sclkfile, - double maxdist, - int debugLevel) { - this.spicemetakernel = spicemetakernel; - this.objfile = objfile; - this.instrument = instrument; - this.sclkfile = sclkfile; - this.maxdist = maxdist; - this.debugLevel = debugLevel; - - coverageMap = new HashMap<>(); - - outputStream = System.out; - } - - /** - * Find facets covered by the FOV of the instrument. For each facet, find the distance and - * position angle of the instrument boresight and the fraction of the facet covered by the FOV. - * - * @param line String where the first "word" is an sclk time - * @throws SpiceException - */ - private void findCoverage(String line) throws SpiceException { - String[] parts = line.split(" "); - if (parts.length == 0) return; - - SCLKTime sclkTime = new SCLKTime(new SCLK(SC_ID), parts[0]); - TDBTime tdbTime = new TDBTime(sclkTime.getTDBSeconds()); - - Instrument instrument = new Instrument(instID); - FOV fov = new FOV(instrument); - Matrix33 instrToBodyFixed = - fov.getReferenceFrame().getPositionTransformation(BodyFixed, tdbTime); - Vector3 bsightBodyFixed = instrToBodyFixed.mxv(fov.getBoresight()); - - StateRecord sr = - new StateRecord( - new Body(SC_ID_String), - tdbTime, - BodyFixed, - new AberrationCorrection("LT+S"), - new Body(TARGET)); - Vector3 scposBodyFixed = sr.getPosition(); - - PositionVector sunPos = - new StateRecord( - new Body("SUN"), - tdbTime, - BodyFixed, - new AberrationCorrection("LT+S"), - new Body(TARGET)) - .getPosition(); - - double[] double3 = new double[3]; - long cellID = - smallBodyModel.computeRayIntersection( - scposBodyFixed.toArray(), bsightBodyFixed.hat().toArray(), double3); - if (cellID == -1) return; // no boresight intersection - - Vector3 bsightIntersectVector = new Vector3(double3); - - if (debugLevel > 1) { - LatitudinalCoordinates lc = new LatitudinalCoordinates(bsightIntersectVector); - System.out.printf( - "# %s %f %f %s\n", - sclkTime, - Math.toDegrees(lc.getLatitude()), - Math.toDegrees(lc.getLongitude()), - bsightIntersectVector); + return TerrasaurTool.super.fullDescription(options, header, footer); } - // flag is 1 if any portion of the spot does not intersect the surface - int flag = 0; - Vector boundaryBodyFixed = new Vector<>(); - if (fov.getShape().equals("RECTANGLE") || fov.getShape().equals("POLYGON")) { - for (Vector3 boundary : fov.getBoundary()) { - boundaryBodyFixed.add(instrToBodyFixed.mxv(boundary)); - } - } else if (fov.getShape().equals("CIRCLE")) { - // bounds contains a single vector parallel to a ray that lies in the cone - // that makes up the boundary of the FOV - Vector3[] bounds = fov.getBoundary(); + private String spicemetakernel; + private String objfile; + private String instrument; + private String sclkfile; + private int debugLevel; + private double maxdist; + private vtkPolyData polydata; + private SmallBodyModel smallBodyModel; + private HashMap coverageMap; - for (int i = 0; i < 8; i++) { - // not ideal, but check every 45 degrees along the perimeter of the circle for intersection - // with the - // surface - Matrix33 rotateAlongPerimeter = new Matrix33(fov.getBoresight(), i * Math.toRadians(45)); - Vector3 perimeterVector = rotateAlongPerimeter.mxv(bounds[0]); - boundaryBodyFixed.add(instrToBodyFixed.mxv(perimeterVector)); - } - } else { - // TODO: add ELLIPSE - System.err.printf( - "Instrument %s: Unsupported FOV shape %s\n", instrument.getName(), fov.getShape()); - System.exit(0); + private int SC_ID; + private String SC_ID_String; + private ReferenceFrame BodyFixed; + private String TARGET; + private final Vector3 NORTH = new Vector3(0, 0, 1e6); + private int instID; + + private PrintStream outputStream; + + public GetSpots( + String spicemetakernel, + String objfile, + String instrument, + String sclkfile, + double maxdist, + int debugLevel) { + this.spicemetakernel = spicemetakernel; + this.objfile = objfile; + this.instrument = instrument; + this.sclkfile = sclkfile; + this.maxdist = maxdist; + this.debugLevel = debugLevel; + + coverageMap = new HashMap<>(); + + outputStream = System.out; } - // check all of the boundary vectors for surface intersections - for (Vector3 vector : boundaryBodyFixed) { - cellID = - smallBodyModel.computeRayIntersection( - scposBodyFixed.toArray(), vector.hat().toArray(), double3); - if (cellID == -1) { - flag = 1; - break; - } - } + /** + * Find facets covered by the FOV of the instrument. For each facet, find the distance and + * position angle of the instrument boresight and the fraction of the facet covered by the FOV. + * + * @param line String where the first "word" is an sclk time + * @throws SpiceException + */ + private void findCoverage(String line) throws SpiceException { + String[] parts = line.split(" "); + if (parts.length == 0) return; - vtkIdList idList = new vtkIdList(); - for (int i = 0; i < polydata.GetNumberOfCells(); ++i) { + SCLKTime sclkTime = new SCLKTime(new SCLK(SC_ID), parts[0]); + TDBTime tdbTime = new TDBTime(sclkTime.getTDBSeconds()); - polydata.GetCellPoints(i, idList); - double[] pt0 = polydata.GetPoint(idList.GetId(0)); - double[] pt1 = polydata.GetPoint(idList.GetId(1)); - double[] pt2 = polydata.GetPoint(idList.GetId(2)); + Instrument instrument = new Instrument(instID); + FOV fov = new FOV(instrument); + Matrix33 instrToBodyFixed = fov.getReferenceFrame().getPositionTransformation(BodyFixed, tdbTime); + Vector3 bsightBodyFixed = instrToBodyFixed.mxv(fov.getBoresight()); - CellInfo ci = CellInfo.getCellInfo(polydata, i, idList); - Vector3 facetNormal = MathConversions.toVector3(ci.normal()); - Vector3 facetCenter = MathConversions.toVector3(ci.center()); + StateRecord sr = new StateRecord( + new Body(SC_ID_String), tdbTime, BodyFixed, new AberrationCorrection("LT+S"), new Body(TARGET)); + Vector3 scposBodyFixed = sr.getPosition(); - // check that facet faces the observer - Vector3 facetToSC = scposBodyFixed.sub(facetCenter); - double emission = facetToSC.sep(facetNormal); - if (emission > Math.PI / 2) continue; + PositionVector sunPos = new StateRecord( + new Body("SUN"), tdbTime, BodyFixed, new AberrationCorrection("LT+S"), new Body(TARGET)) + .getPosition(); - double dist = - findDist(scposBodyFixed, bsightIntersectVector, facetCenter) * 1e3; // milliradians - if (dist < maxdist) { + double[] double3 = new double[3]; + long cellID = smallBodyModel.computeRayIntersection( + scposBodyFixed.toArray(), bsightBodyFixed.hat().toArray(), double3); + if (cellID == -1) return; // no boresight intersection - Vector3 facetToSun = sunPos.sub(facetCenter); - double incidence = facetToSun.sep(facetNormal); - double phase = facetToSun.sep(facetToSC); + Vector3 bsightIntersectVector = new Vector3(double3); - Vector3 pt0v = new Vector3(pt0); - Vector3 pt1v = new Vector3(pt1); - Vector3 pt2v = new Vector3(pt2); - - Vector3 span1 = pt1v.sub(pt0v); - Vector3 span2 = pt2v.sub(pt0v); - - Plane facetPlane = new Plane(pt0v, span1, span2); - Vector3 localNorth = facetPlane.project(NORTH).sub(facetCenter); - Vector3 bsightIntersectProjection = - facetPlane.project(bsightIntersectVector).sub(facetCenter); - - // 0 = North, 90 = East - double pos = - Math.toDegrees(Math.acos(localNorth.hat().dot(bsightIntersectProjection.hat()))); - if (localNorth.cross(bsightIntersectProjection).dot(facetNormal) > 0) pos = 360 - pos; - - int nCovered = 0; - if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt0v.sub(scposBodyFixed)))) nCovered++; - if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt1v.sub(scposBodyFixed)))) nCovered++; - if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt2v.sub(scposBodyFixed)))) nCovered++; - double frac; - if (nCovered == 3) { - frac = 1; - } else { - final double sep012 = span1.negate().sep(pt2v.sub(pt1v)); // angle at vertex 1 - final double sep021 = span2.negate().sep(pt1v.sub(pt2v)); // angle at vertex 2 - - // check 0.5*nPts^2 points if they fall in FOV - int nPts = 50; - Vector pointsInFacet = new Vector<>(); - for (int ii = 0; ii < nPts; ii++) { - Vector3 x = pt0v.add(span1.scale(ii / (nPts - 1.))); - for (int jj = 0; jj < nPts; jj++) { - Vector3 y = x.add(span2.scale(jj / (nPts - 1.))); - - // if outside the facet, angle 01y will be larger than angle 012 - if (span1.negate().sep(y.sub(pt1v)) > sep012) continue; - // if outside the facet, angle 02y will be larger than angle 021 - if (span2.negate().sep(y.sub(pt2v)) > sep021) continue; - pointsInFacet.add(instrToBodyFixed.mtxv(y.sub(scposBodyFixed))); - } - } - - if (pointsInFacet.isEmpty()) { - frac = 0; - } else { - List isInFOV = SPICEUtil.isInFOV(fov, pointsInFacet); - nCovered = 0; - for (boolean b : isInFOV) if (b) nCovered++; - frac = ((double) nCovered) / pointsInFacet.size(); - } - } - - StringBuilder output = - new StringBuilder( - String.format( - "%s %.4f %5.1f %.1f %d %.1f %.1f %.1f", + if (debugLevel > 1) { + LatitudinalCoordinates lc = new LatitudinalCoordinates(bsightIntersectVector); + System.out.printf( + "# %s %f %f %s\n", sclkTime, - dist, - pos, - frac * 100, - flag, - Math.toDegrees(incidence), - Math.toDegrees(emission), - Math.toDegrees(phase))); - for (int j = 1; j < parts.length; j++) output.append(String.format(" %s", parts[j])); - output.append("\n"); - String coverage = coverageMap.get(i); - if (coverage == null) { - coverageMap.put(i, output.toString()); - } else { - coverage += output; - coverageMap.put(i, coverage); + Math.toDegrees(lc.getLatitude()), + Math.toDegrees(lc.getLongitude()), + bsightIntersectVector); } - } - } - } - /** - * Find the angular distance between pt1 and pt2 as seen from scPos. All coordinates are in the - * body fixed frame. - * - * @param scPos Spacecraft position - * @param pt1 Point 1 - * @param pt2 Point 2 - * @return distance between pt1 and pt2 in radians. - */ - private double findDist(Vector3 scPos, Vector3 pt1, Vector3 pt2) { - Vector3 scToPt1 = pt1.sub(scPos).hat(); - Vector3 scToPt2 = pt2.sub(scPos).hat(); + // flag is 1 if any portion of the spot does not intersect the surface + int flag = 0; + Vector boundaryBodyFixed = new Vector<>(); + if (fov.getShape().equals("RECTANGLE") || fov.getShape().equals("POLYGON")) { + for (Vector3 boundary : fov.getBoundary()) { + boundaryBodyFixed.add(instrToBodyFixed.mxv(boundary)); + } + } else if (fov.getShape().equals("CIRCLE")) { + // bounds contains a single vector parallel to a ray that lies in the cone + // that makes up the boundary of the FOV + Vector3[] bounds = fov.getBoundary(); - return Math.acos(scToPt1.dot(scToPt2)); - } + for (int i = 0; i < 8; i++) { + // not ideal, but check every 45 degrees along the perimeter of the circle for intersection + // with the + // surface + Matrix33 rotateAlongPerimeter = new Matrix33(fov.getBoresight(), i * Math.toRadians(45)); + Vector3 perimeterVector = rotateAlongPerimeter.mxv(bounds[0]); + boundaryBodyFixed.add(instrToBodyFixed.mxv(perimeterVector)); + } + } else { + // TODO: add ELLIPSE + System.err.printf("Instrument %s: Unsupported FOV shape %s\n", instrument.getName(), fov.getShape()); + System.exit(0); + } - public void printMap() { - if (debugLevel > 0) { - for (int i = 0; i < polydata.GetNumberOfCells(); ++i) { - outputStream.printf("F%d\n", i + 1); - String output = coverageMap.get(i); - if (output != null) outputStream.print(coverageMap.get(i)); - } - } else { - List list = new ArrayList<>(coverageMap.keySet()); - Collections.sort(list); - for (Integer i : list) { - outputStream.printf("F%d\n", i + 1); - outputStream.print(coverageMap.get(i)); - } - } - outputStream.println("END"); - } + // check all of the boundary vectors for surface intersections + for (Vector3 vector : boundaryBodyFixed) { + cellID = smallBodyModel.computeRayIntersection( + scposBodyFixed.toArray(), vector.hat().toArray(), double3); + if (cellID == -1) { + flag = 1; + break; + } + } - public void process() throws Exception { - boolean useNEAR = false; - if (instrument.equalsIgnoreCase("OLA_LOW")) { - // instID = -64400; // ORX_OLA_BASE - // instID = -64401; // ORX_OLA_ART - instID = -64403; // ORX_OLA_LOW - } - if (instrument.equalsIgnoreCase("OLA_HIGH")) { - instID = -64402; // ORX_OLA_HIGH - } else if (instrument.equalsIgnoreCase("OTES")) { - instID = -64310; // ORX_OTES - } else if (instrument.equalsIgnoreCase("OVIRS_SCI")) { - // instID = -64320; // ORX_OVIRS <- no instrument kernel for this - instID = -64321; // ORX_OVIRS_SCI - // instID = -64322; // ORX_OVIRS_SUN - } else if (instrument.equalsIgnoreCase("REXIS")) { - instID = -64330; // ORX_REXIS - } else if (instrument.equalsIgnoreCase("REXIS_SXM")) { - instID = -64340; // ORX_REXIS_SXM - } else if (instrument.equalsIgnoreCase("POLYCAM")) { - instID = -64360; // ORX_OCAMS_POLYCAM - } else if (instrument.equalsIgnoreCase("MAPCAM")) { - instID = -64361; // ORX_OCAMS_MAPCAM - } else if (instrument.equalsIgnoreCase("SAMCAM")) { - instID = -64362; // ORX_OCAMS_SAMCAM - } else if (instrument.equalsIgnoreCase("NAVCAM")) { - // instID = -64070; // ORX_NAVCAM <- no frame kernel for this - // instID = -64081; // ORX_NAVCAM1 <- no instrument kernel for this - instID = -64082; // ORX_NAVCAM2 <- no instrument kernel for this - } else if (instrument.equalsIgnoreCase("NIS_RECT")) { - useNEAR = true; - // instID = -93021; - instID = -93023; // relative to NEAR_NIS_BASE - } else if (instrument.equalsIgnoreCase("NIS_SQUARE")) { - useNEAR = true; - // instID = -93022; - instID = -93024; // relative to NEAR_NIS_BASE + vtkIdList idList = new vtkIdList(); + for (int i = 0; i < polydata.GetNumberOfCells(); ++i) { + + polydata.GetCellPoints(i, idList); + double[] pt0 = polydata.GetPoint(idList.GetId(0)); + double[] pt1 = polydata.GetPoint(idList.GetId(1)); + double[] pt2 = polydata.GetPoint(idList.GetId(2)); + + CellInfo ci = CellInfo.getCellInfo(polydata, i, idList); + Vector3 facetNormal = MathConversions.toVector3(ci.normal()); + Vector3 facetCenter = MathConversions.toVector3(ci.center()); + + // check that facet faces the observer + Vector3 facetToSC = scposBodyFixed.sub(facetCenter); + double emission = facetToSC.sep(facetNormal); + if (emission > Math.PI / 2) continue; + + double dist = findDist(scposBodyFixed, bsightIntersectVector, facetCenter) * 1e3; // milliradians + if (dist < maxdist) { + + Vector3 facetToSun = sunPos.sub(facetCenter); + double incidence = facetToSun.sep(facetNormal); + double phase = facetToSun.sep(facetToSC); + + Vector3 pt0v = new Vector3(pt0); + Vector3 pt1v = new Vector3(pt1); + Vector3 pt2v = new Vector3(pt2); + + Vector3 span1 = pt1v.sub(pt0v); + Vector3 span2 = pt2v.sub(pt0v); + + Plane facetPlane = new Plane(pt0v, span1, span2); + Vector3 localNorth = facetPlane.project(NORTH).sub(facetCenter); + Vector3 bsightIntersectProjection = + facetPlane.project(bsightIntersectVector).sub(facetCenter); + + // 0 = North, 90 = East + double pos = Math.toDegrees(Math.acos(localNorth.hat().dot(bsightIntersectProjection.hat()))); + if (localNorth.cross(bsightIntersectProjection).dot(facetNormal) > 0) pos = 360 - pos; + + int nCovered = 0; + if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt0v.sub(scposBodyFixed)))) nCovered++; + if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt1v.sub(scposBodyFixed)))) nCovered++; + if (SPICEUtil.isInFOV(fov, instrToBodyFixed.mtxv(pt2v.sub(scposBodyFixed)))) nCovered++; + double frac; + if (nCovered == 3) { + frac = 1; + } else { + final double sep012 = span1.negate().sep(pt2v.sub(pt1v)); // angle at vertex 1 + final double sep021 = span2.negate().sep(pt1v.sub(pt2v)); // angle at vertex 2 + + // check 0.5*nPts^2 points if they fall in FOV + int nPts = 50; + Vector pointsInFacet = new Vector<>(); + for (int ii = 0; ii < nPts; ii++) { + Vector3 x = pt0v.add(span1.scale(ii / (nPts - 1.))); + for (int jj = 0; jj < nPts; jj++) { + Vector3 y = x.add(span2.scale(jj / (nPts - 1.))); + + // if outside the facet, angle 01y will be larger than angle 012 + if (span1.negate().sep(y.sub(pt1v)) > sep012) continue; + // if outside the facet, angle 02y will be larger than angle 021 + if (span2.negate().sep(y.sub(pt2v)) > sep021) continue; + pointsInFacet.add(instrToBodyFixed.mtxv(y.sub(scposBodyFixed))); + } + } + + if (pointsInFacet.isEmpty()) { + frac = 0; + } else { + List isInFOV = SPICEUtil.isInFOV(fov, pointsInFacet); + nCovered = 0; + for (boolean b : isInFOV) if (b) nCovered++; + frac = ((double) nCovered) / pointsInFacet.size(); + } + } + + StringBuilder output = new StringBuilder(String.format( + "%s %.4f %5.1f %.1f %d %.1f %.1f %.1f", + sclkTime, + dist, + pos, + frac * 100, + flag, + Math.toDegrees(incidence), + Math.toDegrees(emission), + Math.toDegrees(phase))); + for (int j = 1; j < parts.length; j++) output.append(String.format(" %s", parts[j])); + output.append("\n"); + String coverage = coverageMap.get(i); + if (coverage == null) { + coverageMap.put(i, output.toString()); + } else { + coverage += output; + coverageMap.put(i, coverage); + } + } + } } - NativeLibraryLoader.loadVtkLibraries(); - polydata = PolyDataUtil.loadShapeModelAndComputeNormals(objfile); - smallBodyModel = new SmallBodyModel(polydata); + /** + * Find the angular distance between pt1 and pt2 as seen from scPos. All coordinates are in the + * body fixed frame. + * + * @param scPos Spacecraft position + * @param pt1 Point 1 + * @param pt2 Point 2 + * @return distance between pt1 and pt2 in radians. + */ + private double findDist(Vector3 scPos, Vector3 pt1, Vector3 pt2) { + Vector3 scToPt1 = pt1.sub(scPos).hat(); + Vector3 scToPt2 = pt2.sub(scPos).hat(); - NativeLibraryLoader.loadSpiceLibraries(); - CSPICE.furnsh(spicemetakernel); - - if (useNEAR) { - SC_ID = -93; - SC_ID_String = "-93"; // "NEAR"; - TARGET = "2000433"; - BodyFixed = new ReferenceFrame("IAU_EROS"); - } else { - SC_ID = -64; - SC_ID_String = "-64"; // "ORX_SPACECRAFT"; - TARGET = "2101955"; - BodyFixed = new ReferenceFrame("IAU_BENNU"); + return Math.acos(scToPt1.dot(scToPt2)); } - List sclkLines = FileUtils.readLines(new File(sclkfile), Charset.defaultCharset()); - boolean foundBegin = false; - for (String line : sclkLines) { - String trimLine = line.trim(); - if (trimLine.startsWith("#")) continue; - if (trimLine.startsWith("BEGIN")) { - foundBegin = true; - continue; - } - if (foundBegin && !trimLine.startsWith("END")) { - if (trimLine.startsWith("#")) continue; - - findCoverage(trimLine); - } + public void printMap() { + if (debugLevel > 0) { + for (int i = 0; i < polydata.GetNumberOfCells(); ++i) { + outputStream.printf("F%d\n", i + 1); + String output = coverageMap.get(i); + if (output != null) outputStream.print(coverageMap.get(i)); + } + } else { + List list = new ArrayList<>(coverageMap.keySet()); + Collections.sort(list); + for (Integer i : list) { + outputStream.printf("F%d\n", i + 1); + outputStream.print(coverageMap.get(i)); + } + } + outputStream.println("END"); } - } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("spice").required().hasArg().desc("SPICE metakernel").build()); - options.addOption(Option.builder("obj").required().hasArg().desc("Shape file").build()); - options.addOption( - Option.builder("instype") - .required() - .hasArg() - .desc( - "one of OLA_LOW, OLA_HIGH, OTES, OVIRS_SCI, REXIS, REXIS_SXM, POLYCAM, MAPCAM, SAMCAM, or NAVCAM") - .build()); - options.addOption( - Option.builder("sclk") - .required() - .hasArg() - .desc( - """ + public void process() throws Exception { + boolean useNEAR = false; + if (instrument.equalsIgnoreCase("OLA_LOW")) { + // instID = -64400; // ORX_OLA_BASE + // instID = -64401; // ORX_OLA_ART + instID = -64403; // ORX_OLA_LOW + } + if (instrument.equalsIgnoreCase("OLA_HIGH")) { + instID = -64402; // ORX_OLA_HIGH + } else if (instrument.equalsIgnoreCase("OTES")) { + instID = -64310; // ORX_OTES + } else if (instrument.equalsIgnoreCase("OVIRS_SCI")) { + // instID = -64320; // ORX_OVIRS <- no instrument kernel for this + instID = -64321; // ORX_OVIRS_SCI + // instID = -64322; // ORX_OVIRS_SUN + } else if (instrument.equalsIgnoreCase("REXIS")) { + instID = -64330; // ORX_REXIS + } else if (instrument.equalsIgnoreCase("REXIS_SXM")) { + instID = -64340; // ORX_REXIS_SXM + } else if (instrument.equalsIgnoreCase("POLYCAM")) { + instID = -64360; // ORX_OCAMS_POLYCAM + } else if (instrument.equalsIgnoreCase("MAPCAM")) { + instID = -64361; // ORX_OCAMS_MAPCAM + } else if (instrument.equalsIgnoreCase("SAMCAM")) { + instID = -64362; // ORX_OCAMS_SAMCAM + } else if (instrument.equalsIgnoreCase("NAVCAM")) { + // instID = -64070; // ORX_NAVCAM <- no frame kernel for this + // instID = -64081; // ORX_NAVCAM1 <- no instrument kernel for this + instID = -64082; // ORX_NAVCAM2 <- no instrument kernel for this + } else if (instrument.equalsIgnoreCase("NIS_RECT")) { + useNEAR = true; + // instID = -93021; + instID = -93023; // relative to NEAR_NIS_BASE + } else if (instrument.equalsIgnoreCase("NIS_SQUARE")) { + useNEAR = true; + // instID = -93022; + instID = -93024; // relative to NEAR_NIS_BASE + } + + NativeLibraryLoader.loadVtkLibraries(); + polydata = PolyDataUtil.loadShapeModelAndComputeNormals(objfile); + smallBodyModel = new SmallBodyModel(polydata); + + NativeLibraryLoader.loadSpiceLibraries(); + CSPICE.furnsh(spicemetakernel); + + if (useNEAR) { + SC_ID = -93; + SC_ID_String = "-93"; // "NEAR"; + TARGET = "2000433"; + BodyFixed = new ReferenceFrame("IAU_EROS"); + } else { + SC_ID = -64; + SC_ID_String = "-64"; // "ORX_SPACECRAFT"; + TARGET = "2101955"; + BodyFixed = new ReferenceFrame("IAU_BENNU"); + } + + List sclkLines = FileUtils.readLines(new File(sclkfile), Charset.defaultCharset()); + boolean foundBegin = false; + for (String line : sclkLines) { + String trimLine = line.trim(); + if (trimLine.startsWith("#")) continue; + if (trimLine.startsWith("BEGIN")) { + foundBegin = true; + continue; + } + if (foundBegin && !trimLine.startsWith("END")) { + if (trimLine.startsWith("#")) continue; + + findCoverage(trimLine); + } + } + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("spice") + .required() + .hasArg() + .desc("SPICE metakernel") + .build()); + options.addOption( + Option.builder("obj").required().hasArg().desc("Shape file").build()); + options.addOption(Option.builder("instype") + .required() + .hasArg() + .desc("one of OLA_LOW, OLA_HIGH, OTES, OVIRS_SCI, REXIS, REXIS_SXM, POLYCAM, MAPCAM, SAMCAM, or NAVCAM") + .build()); + options.addOption(Option.builder("sclk") + .required() + .hasArg() + .desc( + """ file containing sclk values for instrument observation times. All values between the strings BEGIN and END will be processed. For example: BEGIN 3/605862045.24157 END""") - .build()); - options.addOption( - Option.builder("maxdist") - .required() - .hasArg() - .desc("maximum distance of boresight from facet center in milliradians") - .build()); - options.addOption( - Option.builder("all-facets") - .desc( - "Optional. If present, entries for all facets will be output, even if there is no intersection.") - .build()); - options.addOption( - Option.builder("verbose") - .hasArg() - .desc( - "Optional. A level of 1 is equivalent to -all-facets. A level of 2 or higher will print out the boresight intersection position at each sclk.") - .build()); - return options; - } - - public static void main(String[] args) { - - TerrasaurTool defaultOBJ = new GetSpots(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - String spicemetakernel = cl.getOptionValue("spice"); - String objfile = cl.getOptionValue("obj"); - String instrumenttype = cl.getOptionValue("instype"); - String sclkfile = cl.getOptionValue("sclk"); - double distance = Double.parseDouble(cl.getOptionValue("maxdist")); - int debugLevel = Integer.parseInt(cl.getOptionValue("verbose", "0")); - if (cl.hasOption("all-facets")) debugLevel = debugLevel == 0 ? 1 : debugLevel + 1; - - GetSpots gs = - new GetSpots(spicemetakernel, objfile, instrumenttype, sclkfile, distance, debugLevel); - try { - gs.process(); - gs.printMap(); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); + .build()); + options.addOption(Option.builder("maxdist") + .required() + .hasArg() + .desc("maximum distance of boresight from facet center in milliradians") + .build()); + options.addOption(Option.builder("all-facets") + .desc("Optional. If present, entries for all facets will be output, even if there is no intersection.") + .build()); + options.addOption(Option.builder("verbose") + .hasArg() + .desc( + "Optional. A level of 1 is equivalent to -all-facets. A level of 2 or higher will print out the boresight intersection position at each sclk.") + .build()); + return options; + } + + public static void main(String[] args) { + + TerrasaurTool defaultOBJ = new GetSpots(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + String spicemetakernel = cl.getOptionValue("spice"); + String objfile = cl.getOptionValue("obj"); + String instrumenttype = cl.getOptionValue("instype"); + String sclkfile = cl.getOptionValue("sclk"); + double distance = Double.parseDouble(cl.getOptionValue("maxdist")); + int debugLevel = Integer.parseInt(cl.getOptionValue("verbose", "0")); + if (cl.hasOption("all-facets")) debugLevel = debugLevel == 0 ? 1 : debugLevel + 1; + + GetSpots gs = new GetSpots(spicemetakernel, objfile, instrumenttype, sclkfile, distance, debugLevel); + try { + gs.process(); + gs.printMap(); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } } - } } diff --git a/src/main/java/terrasaur/apps/ImpactLocator.java b/src/main/java/terrasaur/apps/ImpactLocator.java index bfcb8d7..e5ddd17 100644 --- a/src/main/java/terrasaur/apps/ImpactLocator.java +++ b/src/main/java/terrasaur/apps/ImpactLocator.java @@ -50,18 +50,18 @@ import vtk.vtkPolyDataWriter; */ public class ImpactLocator implements UnivariateFunction, TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Calculate impact time and position from a sumFile."; - } + @Override + public String shortDescription() { + return "Calculate impact time and position from a sumFile."; + } - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = - """ + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = + """ Given a sum file, shape model, and spacecraft velocity in the J2000 frame, this program will calculate an impact time and position. @@ -70,717 +70,676 @@ public class ImpactLocator implements UnivariateFunction, TerrasaurTool { NOTE: Do not include a "pinpoint" or impact SPK in the kernels to load."""; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private ReferenceFrame J2000; - private ReferenceFrame bodyFixed; - private SmallBodyModel sbm; - private Double finalHeight; - private Double finalStep; - private TDBTime t0; - private StateVector initialObserverJ2000; - private StateVector initialTargetJ2000; - - private Vector3 observerAccelerationJ2000; - private Vector3 targetAccelerationJ2000; - - private StateVector lastState; - - private vtkPolyData rayBundlePolyData; - private vtkCellArray rayBundleCells; - private vtkPoints rayBundlePoints; - - private ImpactLocator() {} - - public ImpactLocator( - ReferenceFrame J2000, - ReferenceFrame bodyFixed, - SmallBodyModel sbm, - Double finalHeight, - Double finalStep, - TDBTime t0, - StateVector initialObserverJ2000, - StateVector initialTargetJ2000, - TDBTime t1, - StateVector finalObserverJ2000, - StateVector finalTargetJ2000) - throws SpiceErrorException { - this.J2000 = J2000; - this.bodyFixed = bodyFixed; - this.sbm = sbm; - this.finalHeight = finalHeight; - this.finalStep = finalStep; - this.t0 = t0; - this.initialObserverJ2000 = initialObserverJ2000; - this.initialTargetJ2000 = initialTargetJ2000; - - if (t1 == null) { - observerAccelerationJ2000 = new Vector3(); - targetAccelerationJ2000 = new Vector3(); - } else { - double duration = t1.getTDBSeconds() - t0.getTDBSeconds(); - observerAccelerationJ2000 = - finalObserverJ2000 - .getVelocity() - .sub(initialObserverJ2000.getVelocity()) - .scale(1. / duration); - targetAccelerationJ2000 = - finalTargetJ2000.getVelocity().sub(initialTargetJ2000.getVelocity()).scale(1. / duration); + return TerrasaurTool.super.fullDescription(options, header, footer); } - } - /** - * find the body state at time t. Assume a constant velocity in the J2000 frame. - * - * @param et ephemeris time - * @return Body state at time et - */ - public StateVector getStateBodyFixed(TDBTime et) { + private ReferenceFrame J2000; + private ReferenceFrame bodyFixed; + private SmallBodyModel sbm; + private Double finalHeight; + private Double finalStep; + private TDBTime t0; + private StateVector initialObserverJ2000; + private StateVector initialTargetJ2000; - try { - double delta = et.sub(t0).getMeasure(); + private Vector3 observerAccelerationJ2000; + private Vector3 targetAccelerationJ2000; - Vector3 observerPosJ2000 = - initialObserverJ2000 - .getPosition() - .add(initialObserverJ2000.getVelocity().scale(delta)) - .add(observerAccelerationJ2000.scale(0.5 * delta * delta)); + private StateVector lastState; - Vector3 targetPosJ2000 = - initialTargetJ2000 - .getPosition() - .add(initialTargetJ2000.getVelocity().scale(delta)) - .add(targetAccelerationJ2000.scale(0.5 * delta * delta)); + private vtkPolyData rayBundlePolyData; + private vtkCellArray rayBundleCells; + private vtkPoints rayBundlePoints; - Vector3 scPosJ2000 = observerPosJ2000.sub(targetPosJ2000); - Vector3 observerVelJ2000 = - initialObserverJ2000.getVelocity().add(observerAccelerationJ2000.scale(delta)); - Vector3 targetVelJ2000 = - initialTargetJ2000.getVelocity().add(targetAccelerationJ2000.scale(delta)); - Vector3 scVelJ2000 = observerVelJ2000.sub(targetVelJ2000); + private ImpactLocator() {} - StateVector scStateJ2000 = new StateVector(scPosJ2000, scVelJ2000); - StateVector scStateBodyFixed = - new StateVector(J2000.getStateTransformation(bodyFixed, et).mxv(scStateJ2000)); + public ImpactLocator( + ReferenceFrame J2000, + ReferenceFrame bodyFixed, + SmallBodyModel sbm, + Double finalHeight, + Double finalStep, + TDBTime t0, + StateVector initialObserverJ2000, + StateVector initialTargetJ2000, + TDBTime t1, + StateVector finalObserverJ2000, + StateVector finalTargetJ2000) + throws SpiceErrorException { + this.J2000 = J2000; + this.bodyFixed = bodyFixed; + this.sbm = sbm; + this.finalHeight = finalHeight; + this.finalStep = finalStep; + this.t0 = t0; + this.initialObserverJ2000 = initialObserverJ2000; + this.initialTargetJ2000 = initialTargetJ2000; - if (lastState == null) { - lastState = scStateBodyFixed; - rayBundlePolyData = new vtkPolyData(); - rayBundleCells = new vtkCellArray(); - rayBundlePoints = new vtkPoints(); - - rayBundlePolyData.SetPoints(rayBundlePoints); - rayBundlePolyData.SetLines(rayBundleCells); - } - long id0 = rayBundlePoints.InsertNextPoint(lastState.getPosition().toArray()); - long id1 = rayBundlePoints.InsertNextPoint(scStateBodyFixed.getPosition().toArray()); - lastState = scStateBodyFixed; - - vtkLine line = new vtkLine(); - line.GetPointIds().SetId(0, id0); - line.GetPointIds().SetId(1, id1); - - rayBundleCells.InsertNextCell(line); - - return scStateBodyFixed; - - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - } - return null; - } - - /** return range from surface at time t */ - @Override - public double value(double t) { - - TDBTime thisTime = new TDBTime(t); - - StateVector scStateBodyFixed = getStateBodyFixed(thisTime); - - Vector3 closestPoint = - new Vector3(sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); - - Vector3 toSurface = scStateBodyFixed.getPosition().sub(closestPoint); - return toSurface.norm(); - } - - private NavigableSet findTrajectory() { - NavigableSet records = new TreeSet<>(); - try { - - lastState = null; - - TDBTime et = t0; - - StateVector scStateBodyFixed = getStateBodyFixed(et); - - Vector3 closestPoint = - new Vector3(sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); - Vector3 toSurface = scStateBodyFixed.getPosition().sub(closestPoint); - double altitude = toSurface.norm(); - - // time it takes to get halfway to the surface - TDBDuration delta = new TDBDuration(altitude / (2 * scStateBodyFixed.getVelocity().norm())); - - boolean keepGoing = true; - while (keepGoing) { - LatitudinalCoordinates lc = new LatitudinalCoordinates(scStateBodyFixed.getPosition()); - records.add( - new ImpactRecord( - et, - scStateBodyFixed, - new LatitudinalCoordinates(altitude, lc.getLongitude(), lc.getLatitude()))); - - et = et.add(delta); - - scStateBodyFixed = getStateBodyFixed(et); - - closestPoint = new Vector3(sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); - toSurface = scStateBodyFixed.getPosition().sub(closestPoint); - altitude = toSurface.norm(); - - // check that we're still moving towards the target - if (scStateBodyFixed.getPosition().dot(scStateBodyFixed.getVelocity()) > 0) { - logger.warn( - "Stopping at {}; passed closest approach to the body center.", - et.toUTCString("ISOC", 3)); - keepGoing = false; - } - - if (altitude > finalHeight) { - delta = new TDBDuration(toSurface.norm() / (2 * scStateBodyFixed.getVelocity().norm())); - } else if (altitude > finalStep) { - delta = new TDBDuration(finalStep / scStateBodyFixed.getVelocity().norm()); + if (t1 == null) { + observerAccelerationJ2000 = new Vector3(); + targetAccelerationJ2000 = new Vector3(); } else { - keepGoing = false; + double duration = t1.getTDBSeconds() - t0.getTDBSeconds(); + observerAccelerationJ2000 = finalObserverJ2000 + .getVelocity() + .sub(initialObserverJ2000.getVelocity()) + .scale(1. / duration); + targetAccelerationJ2000 = finalTargetJ2000 + .getVelocity() + .sub(initialTargetJ2000.getVelocity()) + .scale(1. / duration); } - } - - LatitudinalCoordinates lc = new LatitudinalCoordinates(scStateBodyFixed.getPosition()); - records.add( - new ImpactRecord( - et, - scStateBodyFixed, - new LatitudinalCoordinates(altitude, lc.getLongitude(), lc.getLatitude()))); - - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - } - return records; - } - - static class ImpactRecord implements Comparable { - TDBTime et; - StateVector scStateBodyFixed; - LatitudinalCoordinates lc; - - private ImpactRecord(TDBTime et, StateVector scStateBodyFixed, LatitudinalCoordinates lc) { - this.et = et; - this.scStateBodyFixed = scStateBodyFixed; - this.lc = lc; } + /** + * find the body state at time t. Assume a constant velocity in the J2000 frame. + * + * @param et ephemeris time + * @return Body state at time et + */ + public StateVector getStateBodyFixed(TDBTime et) { + + try { + double delta = et.sub(t0).getMeasure(); + + Vector3 observerPosJ2000 = initialObserverJ2000 + .getPosition() + .add(initialObserverJ2000.getVelocity().scale(delta)) + .add(observerAccelerationJ2000.scale(0.5 * delta * delta)); + + Vector3 targetPosJ2000 = initialTargetJ2000 + .getPosition() + .add(initialTargetJ2000.getVelocity().scale(delta)) + .add(targetAccelerationJ2000.scale(0.5 * delta * delta)); + + Vector3 scPosJ2000 = observerPosJ2000.sub(targetPosJ2000); + Vector3 observerVelJ2000 = initialObserverJ2000.getVelocity().add(observerAccelerationJ2000.scale(delta)); + Vector3 targetVelJ2000 = initialTargetJ2000.getVelocity().add(targetAccelerationJ2000.scale(delta)); + Vector3 scVelJ2000 = observerVelJ2000.sub(targetVelJ2000); + + StateVector scStateJ2000 = new StateVector(scPosJ2000, scVelJ2000); + StateVector scStateBodyFixed = + new StateVector(J2000.getStateTransformation(bodyFixed, et).mxv(scStateJ2000)); + + if (lastState == null) { + lastState = scStateBodyFixed; + rayBundlePolyData = new vtkPolyData(); + rayBundleCells = new vtkCellArray(); + rayBundlePoints = new vtkPoints(); + + rayBundlePolyData.SetPoints(rayBundlePoints); + rayBundlePolyData.SetLines(rayBundleCells); + } + long id0 = rayBundlePoints.InsertNextPoint(lastState.getPosition().toArray()); + long id1 = rayBundlePoints.InsertNextPoint( + scStateBodyFixed.getPosition().toArray()); + lastState = scStateBodyFixed; + + vtkLine line = new vtkLine(); + line.GetPointIds().SetId(0, id0); + line.GetPointIds().SetId(1, id1); + + rayBundleCells.InsertNextCell(line); + + return scStateBodyFixed; + + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + } + return null; + } + + /** return range from surface at time t */ @Override - public int compareTo(ImpactRecord o) { - try { - return Double.compare(et.getTDBSeconds(), o.et.getTDBSeconds()); - } catch (SpiceErrorException e) { - // completely unnecessary exception - return 0; - } - } - } + public double value(double t) { - private static Vector3 correctForAberration( - Vector3 targetLTS, Body observer, Body target, TDBTime t) throws SpiceException { - RemoveAberration ra = new RemoveAberration(target, observer); + TDBTime thisTime = new TDBTime(t); - return ra.getGeometricPosition(t, targetLTS); - } + StateVector scStateBodyFixed = getStateBodyFixed(thisTime); - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("date") - .hasArgs() - .desc("Initial UTC date. Required if -sumFile is not used.") - .build()); - options.addOption( - Option.builder("finalHeight") - .hasArg() - .desc("Height above surface in meters to consider \"impact\". Default is 1 meter.") - .build()); - options.addOption( - Option.builder("finalStep") - .hasArg() - .desc( - "Continue printing output below finalHeight in increments of approximate finalStep " - + "(in meters) until zero. Default is to stop at finalHeight.") - .build()); - options.addOption( - Option.builder("frame") - .required() - .hasArg() - .desc("Required. Name of body fixed frame.") - .build()); - options.addOption( - Option.builder("instrumentFrame") - .hasArg() - .desc( - "SPICE ID for the camera reference frame. Required if -outputTransform " - + "AND -sumFile are used.") - .build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("objFile") - .required() - .hasArg() - .desc("Required. Name of OBJ shape file.") - .build()); - options.addOption( - Option.builder("observer") - .required() - .hasArg() - .desc("Required. SPICE ID for the impactor.") - .build()); - options.addOption( - Option.builder("observerFrame") - .hasArg() - .desc( - "SPICE ID for the impactor's reference frame. Required if -outputTransform is used.") - .build()); - options.addOption( - Option.builder("outputTransform") - .hasArg() - .desc( - "If present, write out a transform file that can be used by TransformShape to place " - + "coordinates in the spacecraft frame in the body fixed frame. The rotation " - + " is evaluated at the sumfile time. The translation is evaluated at the impact time. " - + "Requires -observerFrame option.") - .build()); - options.addOption( - Option.builder("position") - .hasArg() - .desc( - "Spacecraft to body vector in body fixed coordinates. Units are km. " - + "Spacecraft is at the origin to be consistent with sumFile convention.") - .build()); - options.addOption( - Option.builder("spice") - .required() - .hasArgs() - .desc( - "Required. SPICE metakernel file containing body fixed frame and spacecraft kernels. " - + "Can specify more than one kernel, separated by whitespace.") - .build()); - options.addOption( - Option.builder("sumFile") - .hasArg() - .desc( - "Name of sum file to read. Coordinate system is assumed to be in the body " - + "fixed frame with the spacecraft at the origin.") - .build()); - options.addOption( - Option.builder("target") - .required() - .hasArg() - .desc("Required. SPICE ID for the target.") - .build()); - options.addOption( - Option.builder("trajectory") - .hasArg() - .desc( - "If present, name of output VTK file containing trajectory in body fixed coordinates.") - .build()); - options.addOption( - Option.builder("verbosity") - .hasArg() - .desc("This option does nothing! Use -logLevel instead.") - .build()); - options.addOption( - Option.builder("velocity") - .hasArg() - .desc( - "Spacecraft velocity in J2000 relative to the body. Units are km/s. " - + "If not specified, velocity is calculated using SPICE.") - .build()); - return options; - } + Vector3 closestPoint = + new Vector3(sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new ImpactLocator(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadVtkLibraries(); - NativeLibraryLoader.loadSpiceLibraries(); - - String objFile = cl.getOptionValue("objFile"); - SmallBodyModel sbm = new SmallBodyModel(PolyDataUtil.loadShapeModel(objFile)); - - for (String kernel : cl.getOptionValues("spice")) KernelDatabase.load(kernel); - ReferenceFrame J2000 = new ReferenceFrame("J2000"); - ReferenceFrame bodyFixed = new ReferenceFrame(cl.getOptionValue("frame")); - Body observer = new Body(cl.getOptionValue("observer")); - Body target = new Body(cl.getOptionValue("target")); - - final double finalHeight = - cl.hasOption("finalHeight") - ? Double.parseDouble(cl.getOptionValue("finalHeight")) / 1e3 - : 1e-3; - if (finalHeight <= 0) { - logger.warn("Argument to -finalHeight must be positive!"); - System.exit(0); + Vector3 toSurface = scStateBodyFixed.getPosition().sub(closestPoint); + return toSurface.norm(); } - final double finalStep = - cl.hasOption("finalStep") - ? Double.parseDouble(cl.getOptionValue("finalStep")) / 1e3 - : Double.MAX_VALUE; - if (finalStep <= 0) { - logger.warn("Argument to -finalStep must be positive!"); - System.exit(0); + private NavigableSet findTrajectory() { + NavigableSet records = new TreeSet<>(); + try { + + lastState = null; + + TDBTime et = t0; + + StateVector scStateBodyFixed = getStateBodyFixed(et); + + Vector3 closestPoint = new Vector3( + sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); + Vector3 toSurface = scStateBodyFixed.getPosition().sub(closestPoint); + double altitude = toSurface.norm(); + + // time it takes to get halfway to the surface + TDBDuration delta = new TDBDuration( + altitude / (2 * scStateBodyFixed.getVelocity().norm())); + + boolean keepGoing = true; + while (keepGoing) { + LatitudinalCoordinates lc = new LatitudinalCoordinates(scStateBodyFixed.getPosition()); + records.add(new ImpactRecord( + et, + scStateBodyFixed, + new LatitudinalCoordinates(altitude, lc.getLongitude(), lc.getLatitude()))); + + et = et.add(delta); + + scStateBodyFixed = getStateBodyFixed(et); + + closestPoint = new Vector3( + sbm.findClosestPoint(scStateBodyFixed.getPosition().toArray())); + toSurface = scStateBodyFixed.getPosition().sub(closestPoint); + altitude = toSurface.norm(); + + // check that we're still moving towards the target + if (scStateBodyFixed.getPosition().dot(scStateBodyFixed.getVelocity()) > 0) { + logger.warn( + "Stopping at {}; passed closest approach to the body center.", et.toUTCString("ISOC", 3)); + keepGoing = false; + } + + if (altitude > finalHeight) { + delta = new TDBDuration(toSurface.norm() + / (2 * scStateBodyFixed.getVelocity().norm())); + } else if (altitude > finalStep) { + delta = new TDBDuration( + finalStep / scStateBodyFixed.getVelocity().norm()); + } else { + keepGoing = false; + } + } + + LatitudinalCoordinates lc = new LatitudinalCoordinates(scStateBodyFixed.getPosition()); + records.add(new ImpactRecord( + et, scStateBodyFixed, new LatitudinalCoordinates(altitude, lc.getLongitude(), lc.getLatitude()))); + + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + } + return records; } - // initial spacecraft position relative to target body - Vector3 initialPos = new Vector3(); - TDBTime et = null; - SumFile sumFile = null; - if (cl.hasOption("sumFile")) { - sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); + static class ImpactRecord implements Comparable { + TDBTime et; + StateVector scStateBodyFixed; + LatitudinalCoordinates lc; - et = new TDBTime(sumFile.utcString()); + private ImpactRecord(TDBTime et, StateVector scStateBodyFixed, LatitudinalCoordinates lc) { + this.et = et; + this.scStateBodyFixed = scStateBodyFixed; + this.lc = lc; + } - Matrix33 bodyFixedToJ2000 = bodyFixed.getPositionTransformation(J2000, et); - Vector3 scObjJ2000 = bodyFixedToJ2000.mxv(MathConversions.toVector3(sumFile.scobj())); - initialPos = correctForAberration(scObjJ2000, observer, target, et); - initialPos = bodyFixedToJ2000.mtxv(initialPos).negate(); - - } else if (cl.hasOption("date")) { - String[] parts = cl.getOptionValues("date"); - StringBuilder sb = new StringBuilder(); - for (String part : parts) sb.append(part).append(" "); - et = new TDBTime(sb.toString()); - } else { - logger.warn("Either -sumFile or -date must be specified."); - System.exit(0); - } - TDBTime et0 = et; - AberrationCorrection abCorrNone = new AberrationCorrection("NONE"); - - // target's state relative to observer - StateRecord sr = new StateRecord(target, et, bodyFixed, abCorrNone, observer); - - /*- - // aberration test - sr = new StateRecord(target, et, J2000, abCorrNone, observer); - StateRecord srLTS = - new StateRecord(target, et, J2000, new AberrationCorrection("LT+S"), observer); - RemoveAberration ra = new RemoveAberration(target, observer); - Vector3 estimatedGeometric = ra.getGeometricPosition(et, srLTS.getPosition()); - - System.out.printf("LT+S position: %s\n", new Vector3(srLTS.getPosition())); - System.out.printf("geometric position: %s\n", new Vector3(sr.getPosition())); - Vector3 difference = sr.getPosition().sub(srLTS.getPosition()); - System.out.printf("difference: %s %f\n", difference, difference.norm()); - System.out.printf("aberration angle: %.3e\n", srLTS.getPosition().sep(sr.getPosition())); - System.out.printf("estimated geometric: %s\n", estimatedGeometric); - difference = sr.getPosition().sub(estimatedGeometric); - System.out.printf("difference: %s %f\n", difference, difference.norm()); - System.out.printf("angle: %.3e\n", estimatedGeometric.sep(sr.getPosition())); - System.out.println(); - System.exit(0); - */ - - // this is only true with aberration correction NONE! - Vector3 scPosBodyFixed = sr.getPosition().negate(); - - if (cl.hasOption("position")) { - String[] parts = cl.getOptionValue("position").split(","); - double[] tmp = new double[3]; - for (int i = 0; i < 3; i++) tmp[i] = Double.parseDouble(parts[i].trim()); - initialPos.assign(tmp); - } else if (!cl.hasOption("sumFile")) { - // use position calculated by SPICE - initialPos = scPosBodyFixed; + @Override + public int compareTo(ImpactRecord o) { + try { + return Double.compare(et.getTDBSeconds(), o.et.getTDBSeconds()); + } catch (SpiceErrorException e) { + // completely unnecessary exception + return 0; + } + } } - if (Math.abs(scPosBodyFixed.sub(initialPos).norm()) > 0) { - logger.warn( - String.format( - "Warning! Spacecraft position relative to target from SPICE is %s while input position is %s", - new Vector3(scPosBodyFixed), initialPos.toString())); - logger.warn(String.format("Difference is %e km", initialPos.sub(scPosBodyFixed).norm())); - logger.warn("Continuing with input position"); + private static Vector3 correctForAberration(Vector3 targetLTS, Body observer, Body target, TDBTime t) + throws SpiceException { + RemoveAberration ra = new RemoveAberration(target, observer); + + return ra.getGeometricPosition(t, targetLTS); } - Vector3 initialPosJ2000 = bodyFixed.getPositionTransformation(J2000, et).mxv(initialPos); - - // relative to solar system barycenter in J2000 - StateVector initialTargetJ2000 = - new StateRecord(target, et, J2000, abCorrNone, new Body(0)).getStateVector(); - StateVector initialObserverJ2000 = - new StateVector( - initialPosJ2000.add(initialTargetJ2000.getPosition()), - new StateRecord(observer, et, J2000, abCorrNone, new Body(0)).getVelocity()); - - if (cl.hasOption("velocity")) { - String[] parts = cl.getOptionValue("velocity").split(","); - double[] tmp = new double[3]; - for (int i = 0; i < 3; i++) tmp[i] = Double.parseDouble(parts[i].trim()); - Vector3 scVelJ2000 = new Vector3(tmp); - - initialObserverJ2000 = - new StateVector( - initialObserverJ2000.getPosition(), scVelJ2000.add(initialTargetJ2000.getVelocity())); - - StateRecord obs = new StateRecord(observer, et, J2000, abCorrNone, new Body(0)); - logger.info( - String.format( - "spacecraft velocity relative to target from SPICE at %s is %s", - et.toUTCString("ISOC", 3), obs.getVelocity().sub(initialTargetJ2000.getVelocity()))); - logger.info(String.format("Specified velocity is %s", scVelJ2000)); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("date") + .hasArgs() + .desc("Initial UTC date. Required if -sumFile is not used.") + .build()); + options.addOption(Option.builder("finalHeight") + .hasArg() + .desc("Height above surface in meters to consider \"impact\". Default is 1 meter.") + .build()); + options.addOption(Option.builder("finalStep") + .hasArg() + .desc("Continue printing output below finalHeight in increments of approximate finalStep " + + "(in meters) until zero. Default is to stop at finalHeight.") + .build()); + options.addOption(Option.builder("frame") + .required() + .hasArg() + .desc("Required. Name of body fixed frame.") + .build()); + options.addOption(Option.builder("instrumentFrame") + .hasArg() + .desc("SPICE ID for the camera reference frame. Required if -outputTransform " + + "AND -sumFile are used.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("objFile") + .required() + .hasArg() + .desc("Required. Name of OBJ shape file.") + .build()); + options.addOption(Option.builder("observer") + .required() + .hasArg() + .desc("Required. SPICE ID for the impactor.") + .build()); + options.addOption(Option.builder("observerFrame") + .hasArg() + .desc("SPICE ID for the impactor's reference frame. Required if -outputTransform is used.") + .build()); + options.addOption(Option.builder("outputTransform") + .hasArg() + .desc("If present, write out a transform file that can be used by TransformShape to place " + + "coordinates in the spacecraft frame in the body fixed frame. The rotation " + + " is evaluated at the sumfile time. The translation is evaluated at the impact time. " + + "Requires -observerFrame option.") + .build()); + options.addOption(Option.builder("position") + .hasArg() + .desc("Spacecraft to body vector in body fixed coordinates. Units are km. " + + "Spacecraft is at the origin to be consistent with sumFile convention.") + .build()); + options.addOption(Option.builder("spice") + .required() + .hasArgs() + .desc("Required. SPICE metakernel file containing body fixed frame and spacecraft kernels. " + + "Can specify more than one kernel, separated by whitespace.") + .build()); + options.addOption(Option.builder("sumFile") + .hasArg() + .desc("Name of sum file to read. Coordinate system is assumed to be in the body " + + "fixed frame with the spacecraft at the origin.") + .build()); + options.addOption(Option.builder("target") + .required() + .hasArg() + .desc("Required. SPICE ID for the target.") + .build()); + options.addOption(Option.builder("trajectory") + .hasArg() + .desc("If present, name of output VTK file containing trajectory in body fixed coordinates.") + .build()); + options.addOption(Option.builder("verbosity") + .hasArg() + .desc("This option does nothing! Use -logLevel instead.") + .build()); + options.addOption(Option.builder("velocity") + .hasArg() + .desc("Spacecraft velocity in J2000 relative to the body. Units are km/s. " + + "If not specified, velocity is calculated using SPICE.") + .build()); + return options; } - ImpactLocator ifsf = - new ImpactLocator( - J2000, - bodyFixed, - sbm, - finalHeight, - finalStep, - et, - initialObserverJ2000, - initialTargetJ2000, - null, - null, - null); + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new ImpactLocator(); - NavigableSet records = ifsf.findTrajectory(); - TDBTime first = records.first().et; + Options options = defineOptions(); - StateVector scStateBodyFixed = ifsf.getStateBodyFixed(first); - StateVector scStateJ2000 = - new StateVector(bodyFixed.getStateTransformation(J2000, first).mxv(scStateBodyFixed)); + CommandLine cl = defaultOBJ.parseArgs(args, options); - System.out.printf("T0: %s%n", first.toUTCString("ISOC", 3)); - System.out.printf("Frame %s: %s%n", bodyFixed.getName(), scStateBodyFixed); - System.out.printf("Frame J2000: %s%n", scStateJ2000); - System.out.printf( - "%s: Observer velocity relative to SSB (J2000): %s%n", - first.toUTCString("ISOC", 3), initialObserverJ2000.getVelocity()); + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - // find coverage of observer and target - int numSPK = KernelDatabase.ktotal("SPK"); - double lastTarget = -Double.MAX_VALUE; - double lastObserver = -Double.MAX_VALUE; - for (int i = 0; i < numSPK; i++) { - String filename = KernelDatabase.getFileName(i, "SPK"); - SPK thisSPK = SPK.openForRead(filename); - SpiceWindow coverage = thisSPK.getCoverage(target.getIDCode()); - if (coverage.card() > 0) { - double[] lastInterval = coverage.getInterval(coverage.card() - 1); - lastTarget = Math.max(lastTarget, lastInterval[1]); - logger.debug( - "SPK {}: body {}, last time is {}", - filename, - target.getName(), - new TDBTime(lastTarget).toUTCString("ISOC", 3)); - } - coverage = thisSPK.getCoverage(observer.getIDCode()); - if (coverage.card() > 0) { - double[] lastInterval = coverage.getInterval(coverage.card() - 1); - lastObserver = Math.max(lastObserver, lastInterval[1]); - logger.debug( - "SPK {}: body {}, last time is {}", - filename, - observer.getName(), - new TDBTime(lastObserver).toUTCString("ISOC", 3)); - } - } + NativeLibraryLoader.loadVtkLibraries(); + NativeLibraryLoader.loadSpiceLibraries(); - double lastET = Math.min(records.last().et.getTDBSeconds(), lastTarget); - lastET = Math.min(lastET, lastObserver); - TDBTime last = new TDBTime(lastET); - StateRecord finalObserverJ2000 = - new StateRecord(observer, last, J2000, abCorrNone, new Body(0)); - StateRecord finalTargetJ2000 = new StateRecord(target, last, J2000, abCorrNone, new Body(0)); - System.out.printf( - "%s: Observer velocity relative to SSB (J2000): %s%n", - last.toUTCString("ISOC", 3), finalObserverJ2000.getVelocity()); + String objFile = cl.getOptionValue("objFile"); + SmallBodyModel sbm = new SmallBodyModel(PolyDataUtil.loadShapeModel(objFile)); - double duration = last.getTDBSeconds() - first.getTDBSeconds(); - Vector3 observerAccelerationJ2000 = - finalObserverJ2000 - .getVelocity() - .sub(initialObserverJ2000.getVelocity()) - .scale(1. / duration); - Vector3 targetAccelerationJ2000 = - finalTargetJ2000.getVelocity().sub(initialTargetJ2000.getVelocity()).scale(1. / duration); + for (String kernel : cl.getOptionValues("spice")) KernelDatabase.load(kernel); + ReferenceFrame J2000 = new ReferenceFrame("J2000"); + ReferenceFrame bodyFixed = new ReferenceFrame(cl.getOptionValue("frame")); + Body observer = new Body(cl.getOptionValue("observer")); + Body target = new Body(cl.getOptionValue("target")); - System.out.printf("Estimated time of impact %s\n", last.toUTCString("ISOC", 6)); - System.out.printf("Estimated time to impact %.6f seconds\n", duration); - System.out.printf("Estimated observer acceleration (J2000): %s\n", observerAccelerationJ2000); - System.out.printf("Estimated target acceleration (J2000): %s\n", targetAccelerationJ2000); - System.out.printf( - "observer acceleration relative to target: %s\n", - observerAccelerationJ2000.sub(targetAccelerationJ2000)); - - System.out.println(); - - // Run again with constant accelerations for target and observer - ifsf = - new ImpactLocator( - J2000, - bodyFixed, - sbm, - finalHeight, - finalStep, - first, - initialObserverJ2000, - initialTargetJ2000, - last, - finalObserverJ2000, - finalTargetJ2000); - records = ifsf.findTrajectory(); - - System.out.printf( - "%26s, %13s, %13s, %13s, %13s, %13s, %13s, %12s, %12s, %12s", - "UTC", - "X (km)", - "Y (km)", - "Z (km)", - "VX (km/s)", - "VY (km/s)", - "VZ (km/s)", - "Lat (deg)", - "Lon (deg)", - "Alt (m)\n"); - for (ImpactRecord record : records) { - PositionVector p = record.scStateBodyFixed.getPosition(); - VelocityVector v = record.scStateBodyFixed.getVelocity(); - System.out.printf( - String.format( - "%26s, %13.6e, %13.6e, %13.6e, %13.6e, %13.6e, %13.6e, %12.4f, %12.4f, %12.4f\n", - record.et.toUTCString("ISOC", 6), - p.getElt(0), - p.getElt(1), - p.getElt(2), - v.getElt(0), - v.getElt(1), - v.getElt(2), - Math.toDegrees(record.lc.getLatitude()), - Math.toDegrees(record.lc.getLongitude()), - record.lc.getRadius() * 1e3)); - } - - if (cl.hasOption("trajectory")) { - - File trajectoryFile = new File(cl.getOptionValue("trajectory")); - File parent = trajectoryFile.getParentFile(); - if (parent != null && !parent.exists()) parent.mkdirs(); - - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputData(ifsf.rayBundlePolyData); - writer.SetFileName(cl.getOptionValue("trajectory")); - writer.SetFileTypeToBinary(); - writer.Update(); - } - - if (cl.hasOption("outputTransform")) { - if (cl.hasOption("observerFrame")) { - // evaluate rotation at -date or -sumFile time - - File transformFile = new File(cl.getOptionValue("outputTransform")); - File parent = transformFile.getParentFile(); - if (!parent.exists()) parent.mkdirs(); - - ReferenceFrame scFrame = - new ReferenceFrame(cl.getOptionValue("observerFrame").toUpperCase()); - Matrix33 scToBodyFixed; - - if (sumFile != null) { - - // scToBodyFixed = scFrame.getPositionTransformation(bodyFixed, et0); - // logger.info("scToBody (SPICE):\n" + scToBodyFixed); - - ReferenceFrame instrFrame = null; - if (cl.hasOption("instrumentFrame")) - instrFrame = new ReferenceFrame(cl.getOptionValue("instrumentFrame").toUpperCase()); - if (instrFrame == null) { - logger.error("-instrumentFrame needed for -outputTransform. Exiting."); + final double finalHeight = + cl.hasOption("finalHeight") ? Double.parseDouble(cl.getOptionValue("finalHeight")) / 1e3 : 1e-3; + if (finalHeight <= 0) { + logger.warn("Argument to -finalHeight must be positive!"); System.exit(0); - } - Matrix33 scToCamera = scFrame.getPositionTransformation(instrFrame, et0); - - // DART SPECIFIC!!!!!! TODO: create a Terrasaur config file with project-specific - // defaults, - // like spice kernel, camera flips, etc. - - // flip -1, 2, -3 - - Vector3 row0 = MathConversions.toVector3(sumFile.cx().negate()); - Vector3 row1 = MathConversions.toVector3(sumFile.cy()); - Vector3 row2 = MathConversions.toVector3(sumFile.cz().negate()); - - Matrix33 bodyFixedToCamera = new Matrix33(row0, row1, row2); - - scToBodyFixed = bodyFixedToCamera.mtxm(scToCamera); - - // logger.info("scToBody (SUMFILE):\n" + scToBodyFixed); - } else { - scToBodyFixed = scFrame.getPositionTransformation(bodyFixed, et0); } - PositionVector p = records.last().scStateBodyFixed.getPosition(); - try (PrintWriter pw = new PrintWriter(transformFile)) { + final double finalStep = + cl.hasOption("finalStep") ? Double.parseDouble(cl.getOptionValue("finalStep")) / 1e3 : Double.MAX_VALUE; + if (finalStep <= 0) { + logger.warn("Argument to -finalStep must be positive!"); + System.exit(0); + } - List transform = new ArrayList<>(); - for (int i = 0; i < 3; i++) { + // initial spacecraft position relative to target body + Vector3 initialPos = new Vector3(); + TDBTime et = null; + SumFile sumFile = null; + if (cl.hasOption("sumFile")) { + sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); + + et = new TDBTime(sumFile.utcString()); + + Matrix33 bodyFixedToJ2000 = bodyFixed.getPositionTransformation(J2000, et); + Vector3 scObjJ2000 = bodyFixedToJ2000.mxv(MathConversions.toVector3(sumFile.scobj())); + initialPos = correctForAberration(scObjJ2000, observer, target, et); + initialPos = bodyFixedToJ2000.mtxv(initialPos).negate(); + + } else if (cl.hasOption("date")) { + String[] parts = cl.getOptionValues("date"); StringBuilder sb = new StringBuilder(); - for (int j = 0; j < 3; j++) sb.append(String.format("%f ", scToBodyFixed.getElt(i, j))); - sb.append(String.format("%f ", p.getElt(i))); - transform.add(sb.toString()); - } - transform.add("0 0 0 1"); - StringBuilder sb = new StringBuilder(); - for (String s : transform) sb.append(String.format("%s\n", s)); - - pw.println(sb); + for (String part : parts) sb.append(part).append(" "); + et = new TDBTime(sb.toString()); + } else { + logger.warn("Either -sumFile or -date must be specified."); + System.exit(0); } + TDBTime et0 = et; + AberrationCorrection abCorrNone = new AberrationCorrection("NONE"); + + // target's state relative to observer + StateRecord sr = new StateRecord(target, et, bodyFixed, abCorrNone, observer); + /*- - Pair transform = - CommandLineOptionsUtil.getTransformation(cl.getOptionValue("outputTransform")); - System.out.printf("translate\n\t%s\n\t%s\n", p.toString(), - transform.getFirst().toString()); - System.out.printf("rotate\n\t%s\n\t%s\n", scToBodyFixed.toString(), - transform.getSecond().toString()); - */ - } else { - logger.warn("-observerFrame needed for -outputTransform"); - } + // aberration test + sr = new StateRecord(target, et, J2000, abCorrNone, observer); + StateRecord srLTS = + new StateRecord(target, et, J2000, new AberrationCorrection("LT+S"), observer); + RemoveAberration ra = new RemoveAberration(target, observer); + Vector3 estimatedGeometric = ra.getGeometricPosition(et, srLTS.getPosition()); + + System.out.printf("LT+S position: %s\n", new Vector3(srLTS.getPosition())); + System.out.printf("geometric position: %s\n", new Vector3(sr.getPosition())); + Vector3 difference = sr.getPosition().sub(srLTS.getPosition()); + System.out.printf("difference: %s %f\n", difference, difference.norm()); + System.out.printf("aberration angle: %.3e\n", srLTS.getPosition().sep(sr.getPosition())); + System.out.printf("estimated geometric: %s\n", estimatedGeometric); + difference = sr.getPosition().sub(estimatedGeometric); + System.out.printf("difference: %s %f\n", difference, difference.norm()); + System.out.printf("angle: %.3e\n", estimatedGeometric.sep(sr.getPosition())); + System.out.println(); + System.exit(0); + */ + + // this is only true with aberration correction NONE! + Vector3 scPosBodyFixed = sr.getPosition().negate(); + + if (cl.hasOption("position")) { + String[] parts = cl.getOptionValue("position").split(","); + double[] tmp = new double[3]; + for (int i = 0; i < 3; i++) tmp[i] = Double.parseDouble(parts[i].trim()); + initialPos.assign(tmp); + } else if (!cl.hasOption("sumFile")) { + // use position calculated by SPICE + initialPos = scPosBodyFixed; + } + + if (Math.abs(scPosBodyFixed.sub(initialPos).norm()) > 0) { + logger.warn(String.format( + "Warning! Spacecraft position relative to target from SPICE is %s while input position is %s", + new Vector3(scPosBodyFixed), initialPos.toString())); + logger.warn(String.format( + "Difference is %e km", initialPos.sub(scPosBodyFixed).norm())); + logger.warn("Continuing with input position"); + } + + Vector3 initialPosJ2000 = bodyFixed.getPositionTransformation(J2000, et).mxv(initialPos); + + // relative to solar system barycenter in J2000 + StateVector initialTargetJ2000 = new StateRecord(target, et, J2000, abCorrNone, new Body(0)).getStateVector(); + StateVector initialObserverJ2000 = new StateVector( + initialPosJ2000.add(initialTargetJ2000.getPosition()), + new StateRecord(observer, et, J2000, abCorrNone, new Body(0)).getVelocity()); + + if (cl.hasOption("velocity")) { + String[] parts = cl.getOptionValue("velocity").split(","); + double[] tmp = new double[3]; + for (int i = 0; i < 3; i++) tmp[i] = Double.parseDouble(parts[i].trim()); + Vector3 scVelJ2000 = new Vector3(tmp); + + initialObserverJ2000 = new StateVector( + initialObserverJ2000.getPosition(), scVelJ2000.add(initialTargetJ2000.getVelocity())); + + StateRecord obs = new StateRecord(observer, et, J2000, abCorrNone, new Body(0)); + logger.info(String.format( + "spacecraft velocity relative to target from SPICE at %s is %s", + et.toUTCString("ISOC", 3), obs.getVelocity().sub(initialTargetJ2000.getVelocity()))); + logger.info(String.format("Specified velocity is %s", scVelJ2000)); + } + + ImpactLocator ifsf = new ImpactLocator( + J2000, + bodyFixed, + sbm, + finalHeight, + finalStep, + et, + initialObserverJ2000, + initialTargetJ2000, + null, + null, + null); + + NavigableSet records = ifsf.findTrajectory(); + TDBTime first = records.first().et; + + StateVector scStateBodyFixed = ifsf.getStateBodyFixed(first); + StateVector scStateJ2000 = + new StateVector(bodyFixed.getStateTransformation(J2000, first).mxv(scStateBodyFixed)); + + System.out.printf("T0: %s%n", first.toUTCString("ISOC", 3)); + System.out.printf("Frame %s: %s%n", bodyFixed.getName(), scStateBodyFixed); + System.out.printf("Frame J2000: %s%n", scStateJ2000); + System.out.printf( + "%s: Observer velocity relative to SSB (J2000): %s%n", + first.toUTCString("ISOC", 3), initialObserverJ2000.getVelocity()); + + // find coverage of observer and target + int numSPK = KernelDatabase.ktotal("SPK"); + double lastTarget = -Double.MAX_VALUE; + double lastObserver = -Double.MAX_VALUE; + for (int i = 0; i < numSPK; i++) { + String filename = KernelDatabase.getFileName(i, "SPK"); + SPK thisSPK = SPK.openForRead(filename); + SpiceWindow coverage = thisSPK.getCoverage(target.getIDCode()); + if (coverage.card() > 0) { + double[] lastInterval = coverage.getInterval(coverage.card() - 1); + lastTarget = Math.max(lastTarget, lastInterval[1]); + logger.debug( + "SPK {}: body {}, last time is {}", + filename, + target.getName(), + new TDBTime(lastTarget).toUTCString("ISOC", 3)); + } + coverage = thisSPK.getCoverage(observer.getIDCode()); + if (coverage.card() > 0) { + double[] lastInterval = coverage.getInterval(coverage.card() - 1); + lastObserver = Math.max(lastObserver, lastInterval[1]); + logger.debug( + "SPK {}: body {}, last time is {}", + filename, + observer.getName(), + new TDBTime(lastObserver).toUTCString("ISOC", 3)); + } + } + + double lastET = Math.min(records.last().et.getTDBSeconds(), lastTarget); + lastET = Math.min(lastET, lastObserver); + TDBTime last = new TDBTime(lastET); + StateRecord finalObserverJ2000 = new StateRecord(observer, last, J2000, abCorrNone, new Body(0)); + StateRecord finalTargetJ2000 = new StateRecord(target, last, J2000, abCorrNone, new Body(0)); + System.out.printf( + "%s: Observer velocity relative to SSB (J2000): %s%n", + last.toUTCString("ISOC", 3), finalObserverJ2000.getVelocity()); + + double duration = last.getTDBSeconds() - first.getTDBSeconds(); + Vector3 observerAccelerationJ2000 = finalObserverJ2000 + .getVelocity() + .sub(initialObserverJ2000.getVelocity()) + .scale(1. / duration); + Vector3 targetAccelerationJ2000 = finalTargetJ2000 + .getVelocity() + .sub(initialTargetJ2000.getVelocity()) + .scale(1. / duration); + + System.out.printf("Estimated time of impact %s\n", last.toUTCString("ISOC", 6)); + System.out.printf("Estimated time to impact %.6f seconds\n", duration); + System.out.printf("Estimated observer acceleration (J2000): %s\n", observerAccelerationJ2000); + System.out.printf("Estimated target acceleration (J2000): %s\n", targetAccelerationJ2000); + System.out.printf( + "observer acceleration relative to target: %s\n", + observerAccelerationJ2000.sub(targetAccelerationJ2000)); + + System.out.println(); + + // Run again with constant accelerations for target and observer + ifsf = new ImpactLocator( + J2000, + bodyFixed, + sbm, + finalHeight, + finalStep, + first, + initialObserverJ2000, + initialTargetJ2000, + last, + finalObserverJ2000, + finalTargetJ2000); + records = ifsf.findTrajectory(); + + System.out.printf( + "%26s, %13s, %13s, %13s, %13s, %13s, %13s, %12s, %12s, %12s", + "UTC", + "X (km)", + "Y (km)", + "Z (km)", + "VX (km/s)", + "VY (km/s)", + "VZ (km/s)", + "Lat (deg)", + "Lon (deg)", + "Alt (m)\n"); + for (ImpactRecord record : records) { + PositionVector p = record.scStateBodyFixed.getPosition(); + VelocityVector v = record.scStateBodyFixed.getVelocity(); + System.out.printf(String.format( + "%26s, %13.6e, %13.6e, %13.6e, %13.6e, %13.6e, %13.6e, %12.4f, %12.4f, %12.4f\n", + record.et.toUTCString("ISOC", 6), + p.getElt(0), + p.getElt(1), + p.getElt(2), + v.getElt(0), + v.getElt(1), + v.getElt(2), + Math.toDegrees(record.lc.getLatitude()), + Math.toDegrees(record.lc.getLongitude()), + record.lc.getRadius() * 1e3)); + } + + if (cl.hasOption("trajectory")) { + + File trajectoryFile = new File(cl.getOptionValue("trajectory")); + File parent = trajectoryFile.getParentFile(); + if (parent != null && !parent.exists()) parent.mkdirs(); + + vtkPolyDataWriter writer = new vtkPolyDataWriter(); + writer.SetInputData(ifsf.rayBundlePolyData); + writer.SetFileName(cl.getOptionValue("trajectory")); + writer.SetFileTypeToBinary(); + writer.Update(); + } + + if (cl.hasOption("outputTransform")) { + if (cl.hasOption("observerFrame")) { + // evaluate rotation at -date or -sumFile time + + File transformFile = new File(cl.getOptionValue("outputTransform")); + File parent = transformFile.getParentFile(); + if (!parent.exists()) parent.mkdirs(); + + ReferenceFrame scFrame = + new ReferenceFrame(cl.getOptionValue("observerFrame").toUpperCase()); + Matrix33 scToBodyFixed; + + if (sumFile != null) { + + // scToBodyFixed = scFrame.getPositionTransformation(bodyFixed, et0); + // logger.info("scToBody (SPICE):\n" + scToBodyFixed); + + ReferenceFrame instrFrame = null; + if (cl.hasOption("instrumentFrame")) + instrFrame = new ReferenceFrame( + cl.getOptionValue("instrumentFrame").toUpperCase()); + if (instrFrame == null) { + logger.error("-instrumentFrame needed for -outputTransform. Exiting."); + System.exit(0); + } + Matrix33 scToCamera = scFrame.getPositionTransformation(instrFrame, et0); + + // DART SPECIFIC!!!!!! TODO: create a Terrasaur config file with project-specific + // defaults, + // like spice kernel, camera flips, etc. + + // flip -1, 2, -3 + + Vector3 row0 = MathConversions.toVector3(sumFile.cx().negate()); + Vector3 row1 = MathConversions.toVector3(sumFile.cy()); + Vector3 row2 = MathConversions.toVector3(sumFile.cz().negate()); + + Matrix33 bodyFixedToCamera = new Matrix33(row0, row1, row2); + + scToBodyFixed = bodyFixedToCamera.mtxm(scToCamera); + + // logger.info("scToBody (SUMFILE):\n" + scToBodyFixed); + } else { + scToBodyFixed = scFrame.getPositionTransformation(bodyFixed, et0); + } + + PositionVector p = records.last().scStateBodyFixed.getPosition(); + try (PrintWriter pw = new PrintWriter(transformFile)) { + + List transform = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < 3; j++) sb.append(String.format("%f ", scToBodyFixed.getElt(i, j))); + sb.append(String.format("%f ", p.getElt(i))); + transform.add(sb.toString()); + } + transform.add("0 0 0 1"); + StringBuilder sb = new StringBuilder(); + for (String s : transform) sb.append(String.format("%s\n", s)); + + pw.println(sb); + } + /*- + Pair transform = + CommandLineOptionsUtil.getTransformation(cl.getOptionValue("outputTransform")); + System.out.printf("translate\n\t%s\n\t%s\n", p.toString(), + transform.getFirst().toString()); + System.out.printf("rotate\n\t%s\n\t%s\n", scToBodyFixed.toString(), + transform.getSecond().toString()); + */ + } else { + logger.warn("-observerFrame needed for -outputTransform"); + } + } } - } } diff --git a/src/main/java/terrasaur/apps/Maplet2FITS.java b/src/main/java/terrasaur/apps/Maplet2FITS.java index 23f3c50..44dbf77 100644 --- a/src/main/java/terrasaur/apps/Maplet2FITS.java +++ b/src/main/java/terrasaur/apps/Maplet2FITS.java @@ -73,17 +73,17 @@ import terrasaur.utils.xml.AsciiFile; * @version 1.0 */ public class Maplet2FITS implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Convert a Gaskell maplet to FITS format."; - } + @Override + public String shortDescription() { + return "Convert a Gaskell maplet to FITS format."; + } - @Override - public String fullDescription(Options options) { - String header = - """ + @Override + public String fullDescription(Options options) { + String header = + """ This program converts a maplet file in Gaskell maplet format to a FITS file. The program assumes the Gaskell scale is in units of km. By default the generated FITS cube will contain these 10 planes: @@ -99,789 +99,766 @@ By default the generated FITS cube will contain these 10 planes: 10. quality If the --exclude-position option is provided, then only the height, albedo, sigma and quality planes are saved out."""; - String footer = ""; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - public static class HazardParams { - - public final boolean noHazard; - public final double initialValue; - - private HazardParams(boolean noHazard, double initialValue) { - this.noHazard = noHazard; - this.initialValue = initialValue; - } - } - - /** - * Generate HazardParams object that contains attributes needed for generating the Hazard plane. - * - * @param noHazard - * @param initialVal - * @return - */ - public static HazardParams getHazardParam(boolean noHazard, double initialVal) { - return new HazardParams(noHazard, initialVal); - } - - /** - * Self-contained function call to generate ALTWG FITS files from a given maplet file. Note the - * boolean that determines whether to use the outfile string "as-is" or replace the filename with - * one using the ALTWG naming convention. - * - * @param mapletFile - * @param outfile - * @param productType - * @param excludePosition - * @param sigmasFile - * @param qualityFile - * @param namingConvention - * @throws IOException - * @throws FitsException - */ - public static void run( - String mapletFile, - String outfile, - AltwgDataType productType, - boolean excludePosition, - String fitsConfigFile, - String sigmasFile, - String sigsumFile, - String qualityFile, - String namingConvention, - boolean swapBytes, - double scalFactor, - double sigmaScale, - String mapName, - HazardParams hazParam) - throws IOException, FitsException { - - // sanity check. If no naming convention specified then outfile should be fully qualified path - // to an output file, NOT a directory - if (namingConvention.isEmpty()) { - File outFname = new File(outfile); - if (outFname.isDirectory()) { - String errMesg = - "ERROR! No naming convention specified but output file:" - + outfile - + " is a directory! Must be a full path to an output file!"; - throw new RuntimeException(errMesg); - } + String footer = ""; + return TerrasaurTool.super.fullDescription(options, header, footer); } - DataInputStream is = - new DataInputStream(new BufferedInputStream(new FileInputStream(mapletFile))); - DataInput sigmaInput = null; - BufferedInputStream sigmabis = null; - if (sigmasFile != null) { - System.out.println("Parsing " + sigmasFile + " for sigma values."); - Path filePath = Paths.get(sigmasFile); - if (!Files.exists(filePath)) { - System.out.println( - "WARNING! sigmas file:" - + filePath.toAbsolutePath() - + " not found! Sigmas will default to 0!"); - } else { - sigmabis = - new BufferedInputStream(new FileInputStream(filePath.toAbsolutePath().toString())); - sigmaInput = - swapBytes ? new LittleEndianDataInputStream(sigmabis) : new DataInputStream(sigmabis); - } - } - DataInput qualityInput = null; - BufferedInputStream qualbis = null; - if (qualityFile != null) { - System.out.println("Parsing " + qualityFile + " for quality values."); - Path filePath = Paths.get(qualityFile); - if (!Files.exists(filePath)) { - System.out.println( - "WARNING! quality file:" - + filePath.toAbsolutePath() - + " not found! Quality values will default to 0!"); - } else { - qualbis = - new BufferedInputStream(new FileInputStream(filePath.toAbsolutePath().toString())); - qualityInput = - swapBytes ? new LittleEndianDataInputStream(qualbis) : new DataInputStream(qualbis); - } + public static class HazardParams { + + public final boolean noHazard; + public final double initialValue; + + private HazardParams(boolean noHazard, double initialValue) { + this.noHazard = noHazard; + this.initialValue = initialValue; + } } - double[] V = new double[3]; - double[] ux = new double[3]; - double[] uy = new double[3]; - double[] uz = new double[3]; - - /* - * Copied from Bob Gaskell READ_MAP.f code: + /** + * Generate HazardParams object that contains attributes needed for generating the Hazard plane. * - * bytes 1-2 height/hscale (integer*2 msb) byte 3 relative "albedo" (1-199) (byte) - * - * If there is missing data at any point, both height and albedo are set to zero. - * - * The map array is read row by row from the upper left (i,j = -qsz). Rows are increasing in the - * Uy direction with spacing = scale Columns are increasing in the Ux direction with spacing = - * scale Heights are positive in the Uz direction with units = scale + * @param noHazard + * @param initialVal + * @return */ - - // use the first 4 bytes of the maplet header to store intensity min & dynamic range. - float intensityMin = Binary16.toFloat(BinaryUtils.swap(is.readShort())); - float intensityRange = Binary16.toFloat(BinaryUtils.swap(is.readShort())); - - // advancing byte pointer past some headers. Unused, per Bob's WRITE_MAP.f - is.readByte(); - is.readByte(); - - float scale = is.readFloat(); - short halfsize = BinaryUtils.swap(is.readShort()); - - // x,y,z position uncertainty unit vector * 255. - // per Bob's WRITE_MAP.f - is.readByte(); - is.readByte(); - is.readByte(); - - V[0] = is.readFloat(); - V[1] = is.readFloat(); - V[2] = is.readFloat(); - ux[0] = is.readFloat(); - ux[1] = is.readFloat(); - ux[2] = is.readFloat(); - uy[0] = is.readFloat(); - uy[1] = is.readFloat(); - uy[2] = is.readFloat(); - uz[0] = is.readFloat(); - uz[1] = is.readFloat(); - uz[2] = is.readFloat(); - float hscale = is.readFloat(); - - // magnitude of position uncertainty - is.readFloat(); - - // byte 72 of the maplet header is the version number. OLA uses version numbers < 0 and SPC - // maplets have - // version numbers > 0. A version number of 0 is Bob Gaskell's original maplet format. - byte b = is.readByte(); - logger.info("byte is:" + b); - // boolean isOLAMaplet = (is.readByte() < 0); - boolean isOLAMaplet = (b < 0); - if (isOLAMaplet) { - logger.info("byte72 of maplet header indicates this is an OLA maplet."); - } else { - logger.info("byte72 of maplet header indicates this is an SPC maplet."); + public static HazardParams getHazardParam(boolean noHazard, double initialVal) { + return new HazardParams(noHazard, initialVal); } - logger.info(String.format("V : [%f %f %f]", V[0], V[1], V[2])); - logger.info(String.format("ux: [%f %f %f]", ux[0], ux[1], ux[2])); - logger.info(String.format("uy: [%f %f %f]", uy[0], uy[1], uy[2])); - logger.info(String.format("uz: [%f %f %f]", uz[0], uz[1], uz[2])); - logger.info("halfsize: " + halfsize); - logger.info("scale: " + scale); - logger.info("hscale: " + hscale); - logger.info("AltwgProductType:" + productType.toString()); + /** + * Self-contained function call to generate ALTWG FITS files from a given maplet file. Note the + * boolean that determines whether to use the outfile string "as-is" or replace the filename with + * one using the ALTWG naming convention. + * + * @param mapletFile + * @param outfile + * @param productType + * @param excludePosition + * @param sigmasFile + * @param qualityFile + * @param namingConvention + * @throws IOException + * @throws FitsException + */ + public static void run( + String mapletFile, + String outfile, + AltwgDataType productType, + boolean excludePosition, + String fitsConfigFile, + String sigmasFile, + String sigsumFile, + String qualityFile, + String namingConvention, + boolean swapBytes, + double scalFactor, + double sigmaScale, + String mapName, + HazardParams hazParam) + throws IOException, FitsException { - int totalsize = 2 * halfsize + 1; - int numPlanes = 10; - if (excludePosition) { - numPlanes = 4; - if (!hazParam.noHazard) { - numPlanes = numPlanes + 1; - } - } - double[][][] data = new double[numPlanes][totalsize][totalsize]; - double[][][] llrData = new double[numPlanes][totalsize][totalsize]; - - for (int i = -halfsize; i <= halfsize; ++i) - for (int j = -halfsize; j <= halfsize; ++j) { - - double h = is.readShort() * hscale * scale; - - int n = 0; - int llrIndex = 0; - double[] p = { - V[0] + i * scale * ux[0] + j * scale * uy[0] + h * uz[0], - V[1] + i * scale * ux[1] + j * scale * uy[1] + h * uz[1], - V[2] + i * scale * ux[2] + j * scale * uy[2] + h * uz[2] - }; - LatitudinalVector lv = CoordConverters.convertToLatitudinal(new UnwritableVectorIJK(p)); - if (!excludePosition) { - data[n++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLatitude()); - data[n++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLongitude()); - data[n++][i + halfsize][j + halfsize] = lv.getRadius(); - data[n++][i + halfsize][j + halfsize] = p[0]; - data[n++][i + halfsize][j + halfsize] = p[1]; - data[n++][i + halfsize][j + halfsize] = p[2]; - } else { - llrData[llrIndex++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLatitude()); - llrData[llrIndex++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLongitude()); - llrData[llrIndex++][i + halfsize][j + halfsize] = lv.getRadius(); + // sanity check. If no naming convention specified then outfile should be fully qualified path + // to an output file, NOT a directory + if (namingConvention.isEmpty()) { + File outFname = new File(outfile); + if (outFname.isDirectory()) { + String errMesg = "ERROR! No naming convention specified but output file:" + + outfile + + " is a directory! Must be a full path to an output file!"; + throw new RuntimeException(errMesg); + } } - data[n++][i + halfsize][j + halfsize] = h; + DataInputStream is = new DataInputStream(new BufferedInputStream(new FileInputStream(mapletFile))); + DataInput sigmaInput = null; + BufferedInputStream sigmabis = null; + if (sigmasFile != null) { + System.out.println("Parsing " + sigmasFile + " for sigma values."); + Path filePath = Paths.get(sigmasFile); + if (!Files.exists(filePath)) { + System.out.println( + "WARNING! sigmas file:" + filePath.toAbsolutePath() + " not found! Sigmas will default to 0!"); + } else { + sigmabis = new BufferedInputStream( + new FileInputStream(filePath.toAbsolutePath().toString())); + sigmaInput = swapBytes ? new LittleEndianDataInputStream(sigmabis) : new DataInputStream(sigmabis); + } + } + DataInput qualityInput = null; + BufferedInputStream qualbis = null; + if (qualityFile != null) { + System.out.println("Parsing " + qualityFile + " for quality values."); + Path filePath = Paths.get(qualityFile); + if (!Files.exists(filePath)) { + System.out.println("WARNING! quality file:" + + filePath.toAbsolutePath() + + " not found! Quality values will default to 0!"); + } else { + qualbis = new BufferedInputStream( + new FileInputStream(filePath.toAbsolutePath().toString())); + qualityInput = swapBytes ? new LittleEndianDataInputStream(qualbis) : new DataInputStream(qualbis); + } + } - double albedo = is.readUnsignedByte(); + double[] V = new double[3]; + double[] ux = new double[3]; + double[] uy = new double[3]; + double[] uz = new double[3]; + /* + * Copied from Bob Gaskell READ_MAP.f code: + * + * bytes 1-2 height/hscale (integer*2 msb) byte 3 relative "albedo" (1-199) (byte) + * + * If there is missing data at any point, both height and albedo are set to zero. + * + * The map array is read row by row from the upper left (i,j = -qsz). Rows are increasing in the + * Uy direction with spacing = scale Columns are increasing in the Ux direction with spacing = + * scale Heights are positive in the Uz direction with units = scale + */ + + // use the first 4 bytes of the maplet header to store intensity min & dynamic range. + float intensityMin = Binary16.toFloat(BinaryUtils.swap(is.readShort())); + float intensityRange = Binary16.toFloat(BinaryUtils.swap(is.readShort())); + + // advancing byte pointer past some headers. Unused, per Bob's WRITE_MAP.f + is.readByte(); + is.readByte(); + + float scale = is.readFloat(); + short halfsize = BinaryUtils.swap(is.readShort()); + + // x,y,z position uncertainty unit vector * 255. + // per Bob's WRITE_MAP.f + is.readByte(); + is.readByte(); + is.readByte(); + + V[0] = is.readFloat(); + V[1] = is.readFloat(); + V[2] = is.readFloat(); + ux[0] = is.readFloat(); + ux[1] = is.readFloat(); + ux[2] = is.readFloat(); + uy[0] = is.readFloat(); + uy[1] = is.readFloat(); + uy[2] = is.readFloat(); + uz[0] = is.readFloat(); + uz[1] = is.readFloat(); + uz[2] = is.readFloat(); + float hscale = is.readFloat(); + + // magnitude of position uncertainty + is.readFloat(); + + // byte 72 of the maplet header is the version number. OLA uses version numbers < 0 and SPC + // maplets have + // version numbers > 0. A version number of 0 is Bob Gaskell's original maplet format. + byte b = is.readByte(); + logger.info("byte is:" + b); + // boolean isOLAMaplet = (is.readByte() < 0); + boolean isOLAMaplet = (b < 0); if (isOLAMaplet) { - albedo = albedo / 199. * intensityRange + intensityMin; + logger.info("byte72 of maplet header indicates this is an OLA maplet."); } else { - albedo = albedo / 100.0D; + logger.info("byte72 of maplet header indicates this is an SPC maplet."); } - data[n++][i + halfsize][j + halfsize] = albedo; - // sigmas default to 0 unless a SIGMAS file was specified - float sigmaVal = getSigma(sigmaInput, sigmaScale); + logger.info(String.format("V : [%f %f %f]", V[0], V[1], V[2])); + logger.info(String.format("ux: [%f %f %f]", ux[0], ux[1], ux[2])); + logger.info(String.format("uy: [%f %f %f]", uy[0], uy[1], uy[2])); + logger.info(String.format("uz: [%f %f %f]", uz[0], uz[1], uz[2])); + logger.info("halfsize: " + halfsize); + logger.info("scale: " + scale); + logger.info("hscale: " + hscale); + logger.info("AltwgProductType:" + productType.toString()); - data[n++][i + halfsize][j + halfsize] = sigmaVal; - - // quality defaults to 0 unless a quality file was specified - float qualVal = getQuality(qualityInput); - - data[n++][i + halfsize][j + halfsize] = qualVal; - - if ((excludePosition) && (!hazParam.noHazard)) { - // NFT MLN; includes Hazard plane, initialized to initial value - data[n++][i + halfsize][j + halfsize] = hazParam.initialValue; + int totalsize = 2 * halfsize + 1; + int numPlanes = 10; + if (excludePosition) { + numPlanes = 4; + if (!hazParam.noHazard) { + numPlanes = numPlanes + 1; + } } - } + double[][][] data = new double[numPlanes][totalsize][totalsize]; + double[][][] llrData = new double[numPlanes][totalsize][totalsize]; - is.close(); - if (sigmabis != null) sigmabis.close(); - if (qualbis != null) qualbis.close(); + for (int i = -halfsize; i <= halfsize; ++i) + for (int j = -halfsize; j <= halfsize; ++j) { - String sigmaSum = null; - if (sigsumFile != null) { - sigmaSum = parseSigsumFile(sigsumFile); + double h = is.readShort() * hscale * scale; + + int n = 0; + int llrIndex = 0; + double[] p = { + V[0] + i * scale * ux[0] + j * scale * uy[0] + h * uz[0], + V[1] + i * scale * ux[1] + j * scale * uy[1] + h * uz[1], + V[2] + i * scale * ux[2] + j * scale * uy[2] + h * uz[2] + }; + LatitudinalVector lv = CoordConverters.convertToLatitudinal(new UnwritableVectorIJK(p)); + if (!excludePosition) { + data[n++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLatitude()); + data[n++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLongitude()); + data[n++][i + halfsize][j + halfsize] = lv.getRadius(); + data[n++][i + halfsize][j + halfsize] = p[0]; + data[n++][i + halfsize][j + halfsize] = p[1]; + data[n++][i + halfsize][j + halfsize] = p[2]; + } else { + llrData[llrIndex++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLatitude()); + llrData[llrIndex++][i + halfsize][j + halfsize] = Math.toDegrees(lv.getLongitude()); + llrData[llrIndex++][i + halfsize][j + halfsize] = lv.getRadius(); + } + + data[n++][i + halfsize][j + halfsize] = h; + + double albedo = is.readUnsignedByte(); + + if (isOLAMaplet) { + albedo = albedo / 199. * intensityRange + intensityMin; + } else { + albedo = albedo / 100.0D; + } + data[n++][i + halfsize][j + halfsize] = albedo; + + // sigmas default to 0 unless a SIGMAS file was specified + float sigmaVal = getSigma(sigmaInput, sigmaScale); + + data[n++][i + halfsize][j + halfsize] = sigmaVal; + + // quality defaults to 0 unless a quality file was specified + float qualVal = getQuality(qualityInput); + + data[n++][i + halfsize][j + halfsize] = qualVal; + + if ((excludePosition) && (!hazParam.noHazard)) { + // NFT MLN; includes Hazard plane, initialized to initial value + data[n++][i + halfsize][j + halfsize] = hazParam.initialValue; + } + } + + is.close(); + if (sigmabis != null) sigmabis.close(); + if (qualbis != null) qualbis.close(); + + String sigmaSum = null; + if (sigsumFile != null) { + sigmaSum = parseSigsumFile(sigsumFile); + } + + // Map headerValues = new LinkedHashMap(); + + // create new fits header builder + FitsHdrBuilder hdrBuilder = FitsHdr.getBuilder(); + + if (fitsConfigFile != null) { + // initialize header cards with values from configfile + hdrBuilder = FitsHdr.configHdrBuilder(fitsConfigFile, hdrBuilder); + } + + if (sigmaSum != null) { + // able to parse sigma summary value. Show this in header builder + String hdrTag = HeaderTag.SIGMA.toString(); + // update sigma summary value + hdrBuilder.setVCbyHeaderTag(HeaderTag.SIGMA, sigmaSum, HeaderTag.SIGMA.comment()); + + // headerValues.put(HeaderTag.SIGMA.toString(), + // new HeaderCard(HeaderTag.SIGMA.toString(), sigmaSum, HeaderTag.SIGMA.comment())); + + // define this SIGMA as a measurement of height uncertainty + + hdrBuilder.setVCbyHeaderTag(HeaderTag.SIG_DEF, "height uncertainty", HeaderTag.SIG_DEF.comment()); + + // headerValues.put(HeaderTag.SIG_DEF.toString(), + // new HeaderCard(HeaderTag.SIG_DEF.toString(), "Uncertainty", HeaderTag.SIG_DEF.comment())); + //// "Definition of the SIGMA uncertainty metric")); + + } + + // set the mapletFile as the DATASRC + hdrBuilder.setVbyHeaderTag(HeaderTag.DATASRCF, new File(mapletFile).getName()); + + // set the MAP_NAME + hdrBuilder.setVbyHeaderTag(HeaderTag.MAP_NAME, mapName); + + // create list describing the planes in the datacube + List planeList = new ArrayList<>(); + + // determine SrcProductType from header builder + String dataSource = SrcProductType.UNKNOWN.toString(); + if (hdrBuilder.containsKey(HeaderTag.DATASRC.toString())) { + HeaderCard srcCard = hdrBuilder.getCard(HeaderTag.DATASRC.toString()); + dataSource = srcCard.getValue(); + } + + // use scalFactor to determine GSD + double gsd = scale * scalFactor; + + // assume all maplets are local, not global. + boolean isGlobal = false; + + // create FitsData object. Stores data and relevant information about the data + FitsDataBuilder fitsDataB = new FitsDataBuilder(data, isGlobal); + FitsData fitsData = fitsDataB + .setV(V) + .setAltProdType(productType) + .setDataSource(dataSource) + .setU(ux, UnitDir.UX) + .setU(uy, UnitDir.UY) + .setU(uz, UnitDir.UZ) + .setScale(scale) + .setGSD(gsd) + .build(); + + if (excludePosition) { + + // define SIG_DEF using just "Uncertainty" for NFT + hdrBuilder.setVCbyHeaderTag(HeaderTag.SIG_DEF, "Uncertainty", HeaderTag.SIG_DEF.comment()); + + // change comment for DQUAL_2 + hdrBuilder.setCbyHeaderTag(HeaderTag.DQUAL_2, "Data Quality Metric: mean residual [m]"); + + planeList.add(PlaneInfo.HEIGHT); + // call this plane ALBEDO even if OLA is the source + planeList.add(PlaneInfo.ALBEDO); + planeList.add(PlaneInfo.SIGMA); + planeList.add(PlaneInfo.QUALITY); + if (!hazParam.noHazard) { + planeList.add(PlaneInfo.HAZARD); + } + + // create llrData object. Stores lat,lon,radius information. Needed to fill out fits header + FitsDataBuilder llrDataB = new FitsDataBuilder(llrData, isGlobal); + FitsData llrFitsData = llrDataB.setV(V) + .setAltProdType(productType) + .setDataSource(dataSource) + .setU(ux, UnitDir.UX) + .setU(uy, UnitDir.UY) + .setU(uz, UnitDir.UZ) + .setScale(scale) + .setGSD(gsd) + .build(); + + // fill out fits header with information in llrFitsData + hdrBuilder.setByFitsData(llrFitsData); + + saveNFTFits(hdrBuilder, fitsData, planeList, namingConvention, outfile); + + } else { + + planeList.add(PlaneInfo.LAT); + planeList.add(PlaneInfo.LON); + planeList.add(PlaneInfo.RAD); + planeList.add(PlaneInfo.X); + planeList.add(PlaneInfo.Y); + planeList.add(PlaneInfo.Z); + planeList.add(PlaneInfo.HEIGHT); + planeList.add(PlaneInfo.ALBEDO); + planeList.add(PlaneInfo.SIGMA); + planeList.add(PlaneInfo.QUALITY); + + saveDTMFits(hdrBuilder, fitsData, planeList, namingConvention, productType, isGlobal, outfile); + } } - // Map headerValues = new LinkedHashMap(); - - // create new fits header builder - FitsHdrBuilder hdrBuilder = FitsHdr.getBuilder(); - - if (fitsConfigFile != null) { - // initialize header cards with values from configfile - hdrBuilder = FitsHdr.configHdrBuilder(fitsConfigFile, hdrBuilder); - } - - if (sigmaSum != null) { - // able to parse sigma summary value. Show this in header builder - String hdrTag = HeaderTag.SIGMA.toString(); - // update sigma summary value - hdrBuilder.setVCbyHeaderTag(HeaderTag.SIGMA, sigmaSum, HeaderTag.SIGMA.comment()); - - // headerValues.put(HeaderTag.SIGMA.toString(), - // new HeaderCard(HeaderTag.SIGMA.toString(), sigmaSum, HeaderTag.SIGMA.comment())); - - // define this SIGMA as a measurement of height uncertainty - - hdrBuilder.setVCbyHeaderTag( - HeaderTag.SIG_DEF, "height uncertainty", HeaderTag.SIG_DEF.comment()); - - // headerValues.put(HeaderTag.SIG_DEF.toString(), - // new HeaderCard(HeaderTag.SIG_DEF.toString(), "Uncertainty", HeaderTag.SIG_DEF.comment())); - //// "Definition of the SIGMA uncertainty metric")); - - } - - // set the mapletFile as the DATASRC - hdrBuilder.setVbyHeaderTag(HeaderTag.DATASRCF, new File(mapletFile).getName()); - - // set the MAP_NAME - hdrBuilder.setVbyHeaderTag(HeaderTag.MAP_NAME, mapName); - - // create list describing the planes in the datacube - List planeList = new ArrayList<>(); - - // determine SrcProductType from header builder - String dataSource = SrcProductType.UNKNOWN.toString(); - if (hdrBuilder.containsKey(HeaderTag.DATASRC.toString())) { - HeaderCard srcCard = hdrBuilder.getCard(HeaderTag.DATASRC.toString()); - dataSource = srcCard.getValue(); - } - - // use scalFactor to determine GSD - double gsd = scale * scalFactor; - - // assume all maplets are local, not global. - boolean isGlobal = false; - - // create FitsData object. Stores data and relevant information about the data - FitsDataBuilder fitsDataB = new FitsDataBuilder(data, isGlobal); - FitsData fitsData = - fitsDataB - .setV(V) - .setAltProdType(productType) - .setDataSource(dataSource) - .setU(ux, UnitDir.UX) - .setU(uy, UnitDir.UY) - .setU(uz, UnitDir.UZ) - .setScale(scale) - .setGSD(gsd) - .build(); - - if (excludePosition) { - - // define SIG_DEF using just "Uncertainty" for NFT - hdrBuilder.setVCbyHeaderTag(HeaderTag.SIG_DEF, "Uncertainty", HeaderTag.SIG_DEF.comment()); - - // change comment for DQUAL_2 - hdrBuilder.setCbyHeaderTag(HeaderTag.DQUAL_2, "Data Quality Metric: mean residual [m]"); - - planeList.add(PlaneInfo.HEIGHT); - // call this plane ALBEDO even if OLA is the source - planeList.add(PlaneInfo.ALBEDO); - planeList.add(PlaneInfo.SIGMA); - planeList.add(PlaneInfo.QUALITY); - if (!hazParam.noHazard) { - planeList.add(PlaneInfo.HAZARD); - } - - // create llrData object. Stores lat,lon,radius information. Needed to fill out fits header - FitsDataBuilder llrDataB = new FitsDataBuilder(llrData, isGlobal); - FitsData llrFitsData = - llrDataB - .setV(V) - .setAltProdType(productType) - .setDataSource(dataSource) - .setU(ux, UnitDir.UX) - .setU(uy, UnitDir.UY) - .setU(uz, UnitDir.UZ) - .setScale(scale) - .setGSD(gsd) - .build(); - - // fill out fits header with information in llrFitsData - hdrBuilder.setByFitsData(llrFitsData); - - saveNFTFits(hdrBuilder, fitsData, planeList, namingConvention, outfile); - - } else { - - planeList.add(PlaneInfo.LAT); - planeList.add(PlaneInfo.LON); - planeList.add(PlaneInfo.RAD); - planeList.add(PlaneInfo.X); - planeList.add(PlaneInfo.Y); - planeList.add(PlaneInfo.Z); - planeList.add(PlaneInfo.HEIGHT); - planeList.add(PlaneInfo.ALBEDO); - planeList.add(PlaneInfo.SIGMA); - planeList.add(PlaneInfo.QUALITY); - - saveDTMFits( - hdrBuilder, fitsData, planeList, namingConvention, productType, isGlobal, outfile); - } - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("input-map").required().hasArg().desc("input maplet file").build()); - options.addOption( - Option.builder("output-fits").required().hasArg().desc("output FITS file").build()); - options.addOption( - Option.builder("exclude-position") - .desc( - "Only save out the height, albedo, sigma, quality, and hazard planes to the output file. Used for creating NFT MLNs.") - .build()); - options.addOption( - Option.builder("noHazard") - .desc( - "Only used in conjunction with -exclude-position. If present then the NFT MLN will NOT include a Hazard plane initially filled with all ones.") - .build()); - options.addOption( - Option.builder("hazardVal") - .hasArg() - .desc( - "Only used in conjunction with -exclude-position. If present then will use the specified value.") - .build()); - options.addOption( - Option.builder("lsb") - .desc( - """ + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("input-map") + .required() + .hasArg() + .desc("input maplet file") + .build()); + options.addOption(Option.builder("output-fits") + .required() + .hasArg() + .desc("output FITS file") + .build()); + options.addOption(Option.builder("exclude-position") + .desc( + "Only save out the height, albedo, sigma, quality, and hazard planes to the output file. Used for creating NFT MLNs.") + .build()); + options.addOption(Option.builder("noHazard") + .desc( + "Only used in conjunction with -exclude-position. If present then the NFT MLN will NOT include a Hazard plane initially filled with all ones.") + .build()); + options.addOption(Option.builder("hazardVal") + .hasArg() + .desc("Only used in conjunction with -exclude-position. If present then will use the specified value.") + .build()); + options.addOption(Option.builder("lsb") + .desc( + """ By default the sigmas and quality binary files are assumed to be in big-endian floating point format. Pass this argument if you know your sigma and quality binary files are in little-endian format. For example, products created by SPC executables are OS dependent and intel Linux OSes use little-endian.""") - .build()); - options.addOption( - Option.builder("scalFactor") - .hasArg() - .desc( - "Enter scale factor used to convert scale to ground sample distance in mm i.e. for SPC maplets the scale factor is 1000000 (km to mm). Set to 1.0e6 by default.") - .build()); - options.addOption( - Option.builder("sigmas-file") - .hasArg() - .desc( - "Path to binary sigmas file containing sigma values per pixel, in same order as the maplet file. If this option is omitted, the sigma plane is set to all zeros.") - .build()); - options.addOption( - Option.builder("sigsum-file") - .hasArg() - .desc( - "Path to ascii sigma summary file, containing the overall sigma value of the maplet.") - .build()); - options.addOption( - Option.builder("sigmaScale") - .hasArg() - .desc( - "Scale sigmas from sigmas-file by . Only applicable if -sigmas-file is used. Defaults to 1 if not specified.") - .build()); - options.addOption( - Option.builder("mapname") - .hasArg() - .desc("Sets the MAP_NAME fits keyword to . Default is 'Non-NFT DTM'") - .build()); - options.addOption( - Option.builder("quality-file") - .hasArg() - .desc( - "Path to binary quality file containing quality values. If this option is omitted, the quality plane is set to all zeros.") - .build()); - options.addOption( - Option.builder("configFile") - .hasArg() - .desc( - """ + .build()); + options.addOption(Option.builder("scalFactor") + .hasArg() + .desc( + "Enter scale factor used to convert scale to ground sample distance in mm i.e. for SPC maplets the scale factor is 1000000 (km to mm). Set to 1.0e6 by default.") + .build()); + options.addOption(Option.builder("sigmas-file") + .hasArg() + .desc( + "Path to binary sigmas file containing sigma values per pixel, in same order as the maplet file. If this option is omitted, the sigma plane is set to all zeros.") + .build()); + options.addOption(Option.builder("sigsum-file") + .hasArg() + .desc("Path to ascii sigma summary file, containing the overall sigma value of the maplet.") + .build()); + options.addOption(Option.builder("sigmaScale") + .hasArg() + .desc( + "Scale sigmas from sigmas-file by . Only applicable if -sigmas-file is used. Defaults to 1 if not specified.") + .build()); + options.addOption(Option.builder("mapname") + .hasArg() + .desc("Sets the MAP_NAME fits keyword to . Default is 'Non-NFT DTM'") + .build()); + options.addOption(Option.builder("quality-file") + .hasArg() + .desc( + "Path to binary quality file containing quality values. If this option is omitted, the quality plane is set to all zeros.") + .build()); + options.addOption(Option.builder("configFile") + .hasArg() + .desc( + """ Path to fits configuration file that contains keywords and values which should be included in the fits header. The fits header will always be fully populated with all keywords that are required by the ALTWG SIS. The values may not be populated or UNK if they cannot be derived from the data itself. This configuration file is a way to populate those values.""") - .build()); - options.addOption( - Option.builder("namingConvention") - .hasArg() - .desc( - """ + .build()); + options.addOption(Option.builder("namingConvention") + .hasArg() + .desc( + """ Renames the output fits file per the naming convention specified by the string. Currently supports 'altproduct', 'dartproduct', and 'altnftmln' conventions. NOTE that -exclude-position does not automatically choose the 'altnftmln' naming convention! 'ALTWG NFT MLN naming convention (altnftmln) MUST BE EXPLICITLY SPECIFIED The renamed file is placed in the path specified by -output-fits""") - .build()); - return options; - } - - public static void main(String[] args) throws FitsException, IOException { - TerrasaurTool defaultOBJ = new Maplet2FITS(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - boolean excludePosition = cl.hasOption("exclude-position"); - boolean noHazard = cl.hasOption("noHazard"); - String namingConvention = cl.getOptionValue("namingConvention", "noneused"); - boolean swapBytes = cl.hasOption("lsb"); - String sigmasFile = cl.hasOption("sigmas-file") ? cl.getOptionValue("sigmas-file") : null; - String sigsumFile = cl.hasOption("sigsum-file") ? cl.getOptionValue("sigsum-file") : null; - String qualityFile = cl.hasOption("quality-file") ? cl.getOptionValue("quality-file") : null; - String fitsConfigFile = cl.hasOption("configFile") ? cl.getOptionValue("configFile") : null; - String mapName = cl.getOptionValue("mapname", "Non-NFT DTM"); - - double scalFactor = - Double.parseDouble(cl.getOptionValue("scalFactor", "1e6").replaceAll("[dD]", "e")); - double sigmaScale = - Double.parseDouble(cl.getOptionValue("sigmaScale", "1.0").replaceAll("[dD]", "e")); - double hazardVal = - Double.parseDouble(cl.getOptionValue("hazardVal", "1.0").replaceAll("[dD]", "e")); - AltwgDataType altwgProduct = AltwgDataType.NA; - - if (sigsumFile != null) logger.info("using {} to parse for global uncertainty.", sigsumFile); - - if (!namingConvention.isEmpty()) { - if (excludePosition) { - altwgProduct = AltwgDataType.NFTDTM; - } else { - altwgProduct = AltwgDataType.DTM; - } + .build()); + return options; } - HazardParams hazParams = getHazardParam(noHazard, hazardVal); + public static void main(String[] args) throws FitsException, IOException { + TerrasaurTool defaultOBJ = new Maplet2FITS(); - String mapletFile = cl.getOptionValue("input-map"); - String outfile = cl.getOptionValue("output-fits"); - logger.info("altwgProductType:{}", altwgProduct.toString()); - run( - mapletFile, - outfile, - altwgProduct, - excludePosition, - fitsConfigFile, - sigmasFile, - sigsumFile, - qualityFile, - namingConvention, - swapBytes, - scalFactor, - sigmaScale, - mapName, - hazParams); - } + Options options = defineOptions(); - public static String parseSigsumFile(String sigsumFile) throws IOException { - if (!Files.exists(Paths.get(sigsumFile))) { - System.out.println( - "Warning! Sigmas summary file:" - + sigsumFile - + " does not exist! Not " - + "able to parse sigma summary."); - return null; - } else { - List content = FileUtils.readLines(new File(sigsumFile), Charset.defaultCharset()); - String firstLine = content.get(0); + CommandLine cl = defaultOBJ.parseArgs(args, options); - if (firstLine != null) { + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - // first assume format is - // this is the SPC format - // try to parse a double from this - String[] array = firstLine.split(" +"); - boolean foundFirst = false; - if (array.length > 1) { - System.out.println("Assuming sigma summary file of form:"); - System.out.println(" "); - for (String s : array) { - if (foundFirst) { - // second cell after finding the first non-empty cell should be the sigma summary - // value! - double thisD = Double.parseDouble(s.replaceAll("[dD]", "e")); - return Double.isNaN(thisD) ? null : Double.toString(thisD); + boolean excludePosition = cl.hasOption("exclude-position"); + boolean noHazard = cl.hasOption("noHazard"); + String namingConvention = cl.getOptionValue("namingConvention", "noneused"); + boolean swapBytes = cl.hasOption("lsb"); + String sigmasFile = cl.hasOption("sigmas-file") ? cl.getOptionValue("sigmas-file") : null; + String sigsumFile = cl.hasOption("sigsum-file") ? cl.getOptionValue("sigsum-file") : null; + String qualityFile = cl.hasOption("quality-file") ? cl.getOptionValue("quality-file") : null; + String fitsConfigFile = cl.hasOption("configFile") ? cl.getOptionValue("configFile") : null; + String mapName = cl.getOptionValue("mapname", "Non-NFT DTM"); + + double scalFactor = + Double.parseDouble(cl.getOptionValue("scalFactor", "1e6").replaceAll("[dD]", "e")); + double sigmaScale = + Double.parseDouble(cl.getOptionValue("sigmaScale", "1.0").replaceAll("[dD]", "e")); + double hazardVal = + Double.parseDouble(cl.getOptionValue("hazardVal", "1.0").replaceAll("[dD]", "e")); + AltwgDataType altwgProduct = AltwgDataType.NA; + + if (sigsumFile != null) logger.info("using {} to parse for global uncertainty.", sigsumFile); + + if (!namingConvention.isEmpty()) { + if (excludePosition) { + altwgProduct = AltwgDataType.NFTDTM; + } else { + altwgProduct = AltwgDataType.DTM; } - if (!s.isEmpty()) { - // loop through array until you find first non-empty string. - // This should be the maplet filename - foundFirst = true; - } - } - } else { - - // assume format is mean on first line, median on second line - if (content.size() > 1) { - System.out.println("Assuming sigma summary file of form:"); - System.out.println(""); - System.out.println(""); - System.out.println("Will use median value"); - return content.get(1).replaceAll("\\s+", ""); - } else { - // first line is not null but there is only one line - System.out.println("Assuming first value in first line contains sigma summary!"); - return content.get(0).replaceAll("\\s+", ""); - } } - // did not find first non-empty string or did not find sigma - System.out.println("WARNING: Could not parse sigma summary file!"); - return null; + HazardParams hazParams = getHazardParam(noHazard, hazardVal); - /* - * firstLine = firstLine.replace(" ", ""); double thisD = StringUtil.parseSafeD(firstLine); - * if (Double.isNaN(thisD)) { Pattern p = Pattern.compile("^\\s*(\\d+\\.?\\d+)\\s*.*"); - * Matcher m = p.matcher(firstLine); if (m.matches()) { return m.group(1); } else { return - * null; } } else { return Double.toString(thisD); } - */ - } else { - return null; - } - } - } - - /** - * Given a maplet, return the command call to Maplet2FITS to turn it into a fits file. Allows one - * to specify filenames of sigma, sigma summary, and quality files. Set string variables to empty - * string if you do not wish to include them in the command call. Method generates the full set of - * fits file planes, i.e. NOT an NFT Fits file. - * - * @param mapletFile - path to maplet - * @param configFile - configuration file to use - * @param lsb - if True assume sigma and quality files are in little-endian - * @param sigmaFile - pointer to sigma file. Leave as empty string if no file exists/needed - * (defaults to 0). - * @param sigmaScaleF - sigma scale factor to be used with sigma values read from sigma file. - * @param sigsumFile - pointer to simga summary file. Leave as empty string if no file - * exists/needed (defaults to 0) - * @param qualityFile - pointer to quality file. Leave as empty string if no file exists/needed - * (defaults to 0). - * @return - */ - public static String getCmd( - File mapletFile, - String outFits, - String configFile, - boolean altwgNaming, - boolean lsb, - String sigmaFile, - String sigsumFile, - String sigmaScaleF, - String qualityFile) { - - StringBuilder toolexe = new StringBuilder("Maplet2FITS"); - if (!configFile.isEmpty()) { - toolexe.append(" -configFile "); - toolexe.append(configFile); + String mapletFile = cl.getOptionValue("input-map"); + String outfile = cl.getOptionValue("output-fits"); + logger.info("altwgProductType:{}", altwgProduct.toString()); + run( + mapletFile, + outfile, + altwgProduct, + excludePosition, + fitsConfigFile, + sigmasFile, + sigsumFile, + qualityFile, + namingConvention, + swapBytes, + scalFactor, + sigmaScale, + mapName, + hazParams); } - if (altwgNaming) { - toolexe.append(" -altwgNaming"); + public static String parseSigsumFile(String sigsumFile) throws IOException { + if (!Files.exists(Paths.get(sigsumFile))) { + System.out.println("Warning! Sigmas summary file:" + + sigsumFile + + " does not exist! Not " + + "able to parse sigma summary."); + return null; + } else { + List content = FileUtils.readLines(new File(sigsumFile), Charset.defaultCharset()); + String firstLine = content.get(0); + + if (firstLine != null) { + + // first assume format is + // this is the SPC format + // try to parse a double from this + String[] array = firstLine.split(" +"); + boolean foundFirst = false; + if (array.length > 1) { + System.out.println("Assuming sigma summary file of form:"); + System.out.println(" "); + for (String s : array) { + if (foundFirst) { + // second cell after finding the first non-empty cell should be the sigma summary + // value! + double thisD = Double.parseDouble(s.replaceAll("[dD]", "e")); + return Double.isNaN(thisD) ? null : Double.toString(thisD); + } + if (!s.isEmpty()) { + // loop through array until you find first non-empty string. + // This should be the maplet filename + foundFirst = true; + } + } + } else { + + // assume format is mean on first line, median on second line + if (content.size() > 1) { + System.out.println("Assuming sigma summary file of form:"); + System.out.println(""); + System.out.println(""); + System.out.println("Will use median value"); + return content.get(1).replaceAll("\\s+", ""); + } else { + // first line is not null but there is only one line + System.out.println("Assuming first value in first line contains sigma summary!"); + return content.get(0).replaceAll("\\s+", ""); + } + } + + // did not find first non-empty string or did not find sigma + System.out.println("WARNING: Could not parse sigma summary file!"); + return null; + + /* + * firstLine = firstLine.replace(" ", ""); double thisD = StringUtil.parseSafeD(firstLine); + * if (Double.isNaN(thisD)) { Pattern p = Pattern.compile("^\\s*(\\d+\\.?\\d+)\\s*.*"); + * Matcher m = p.matcher(firstLine); if (m.matches()) { return m.group(1); } else { return + * null; } } else { return Double.toString(thisD); } + */ + } else { + return null; + } + } } - if (lsb) { - toolexe.append(" -lsb"); + /** + * Given a maplet, return the command call to Maplet2FITS to turn it into a fits file. Allows one + * to specify filenames of sigma, sigma summary, and quality files. Set string variables to empty + * string if you do not wish to include them in the command call. Method generates the full set of + * fits file planes, i.e. NOT an NFT Fits file. + * + * @param mapletFile - path to maplet + * @param configFile - configuration file to use + * @param lsb - if True assume sigma and quality files are in little-endian + * @param sigmaFile - pointer to sigma file. Leave as empty string if no file exists/needed + * (defaults to 0). + * @param sigmaScaleF - sigma scale factor to be used with sigma values read from sigma file. + * @param sigsumFile - pointer to simga summary file. Leave as empty string if no file + * exists/needed (defaults to 0) + * @param qualityFile - pointer to quality file. Leave as empty string if no file exists/needed + * (defaults to 0). + * @return + */ + public static String getCmd( + File mapletFile, + String outFits, + String configFile, + boolean altwgNaming, + boolean lsb, + String sigmaFile, + String sigsumFile, + String sigmaScaleF, + String qualityFile) { + + StringBuilder toolexe = new StringBuilder("Maplet2FITS"); + if (!configFile.isEmpty()) { + toolexe.append(" -configFile "); + toolexe.append(configFile); + } + + if (altwgNaming) { + toolexe.append(" -altwgNaming"); + } + + if (lsb) { + toolexe.append(" -lsb"); + } + + Path thisFile; + if (!sigmaFile.isEmpty()) { + thisFile = Paths.get(sigmaFile); + if (Files.exists(thisFile)) { + toolexe.append(" -sigmas-file "); + toolexe.append(thisFile.toAbsolutePath()); + } else { + logger.warn("Could not find sigmas file:{}", sigmaFile); + } + } + + if (!sigmaScaleF.isEmpty()) { + toolexe.append(" -sigmaScale "); + toolexe.append(sigmaScaleF); + } + + if (!sigsumFile.isEmpty()) { + thisFile = Paths.get(sigsumFile); + if (Files.exists(thisFile)) { + toolexe.append(" -sigsum-file "); + toolexe.append(thisFile.toAbsolutePath()); + } else { + logger.warn("Could not find sigma summary file:{}", sigsumFile); + } + } + + if (!qualityFile.isEmpty()) { + thisFile = Paths.get(qualityFile); + if (Files.exists(thisFile)) { + toolexe.append(" -quality-file "); + toolexe.append(thisFile.toAbsolutePath()); + } else { + logger.warn("Could not find quality file:{}", qualityFile); + } + } + + toolexe.append(" "); + toolexe.append(mapletFile.getAbsolutePath()); + toolexe.append(" "); + toolexe.append(outFits); + + return toolexe.toString(); } - Path thisFile; - if (!sigmaFile.isEmpty()) { - thisFile = Paths.get(sigmaFile); - if (Files.exists(thisFile)) { - toolexe.append(" -sigmas-file "); - toolexe.append(thisFile.toAbsolutePath()); - } else { - logger.warn("Could not find sigmas file:{}", sigmaFile); - } + private static float getSigma(DataInput dataIn, double sigmaScale) throws IOException { + float floatVal = 0f; + if (dataIn != null) { + floatVal = dataIn.readFloat(); + } + floatVal = floatVal * (float) sigmaScale; + return floatVal; } - if (!sigmaScaleF.isEmpty()) { - toolexe.append(" -sigmaScale "); - toolexe.append(sigmaScaleF); + private static float getQuality(DataInput dataIn) throws IOException { + float floatVal = 0f; + if (dataIn != null) { + floatVal = dataIn.readFloat(); + } + return floatVal; } - if (!sigsumFile.isEmpty()) { - thisFile = Paths.get(sigsumFile); - if (Files.exists(thisFile)) { - toolexe.append(" -sigsum-file "); - toolexe.append(thisFile.toAbsolutePath()); - } else { - logger.warn("Could not find sigma summary file:{}", sigsumFile); - } + private static void saveNFTFits( + FitsHdrBuilder hdrBuilder, + FitsData fitsData, + List planeList, + String namingConvention, + String outfile) + throws FitsException, IOException { + + File crossrefFile = null; + + NameConvention nameConvention = NameConvention.parseNameConvention(namingConvention); + if (nameConvention != NameConvention.NONEUSED) { + + String outNFTFname = outfile; + Path outPath = Paths.get(outNFTFname); + + // save old filename + String oldFilename = outPath.getFileName().toString(); + + // hardcode for now. NFT is not a nominal Toolkit product. + AltwgDataType productType = null; + boolean isGlobal = false; + + // generate new NFT output filename based on naming convention + ProductNamer productNamer = NamingFactory.parseNamingConvention(namingConvention); + String newbaseName = productNamer.productbaseName(hdrBuilder, productType, isGlobal); + + String newFilename = newbaseName + ".fits"; + + // replace outfile with new nft filename and write FITS file to it. + outNFTFname = outNFTFname.replace(oldFilename, newFilename); + + // save new PDS name in cross-reference file, for future reference + crossrefFile = new File(outNFTFname + ".crf"); + AsciiFile crfFile = new AsciiFile(crossrefFile.getAbsolutePath()); + crfFile.streamSToFile(newbaseName, 0); + crfFile.closeFile(); + + outfile = outNFTFname; + } + + FitsHeaderType hdrType = FitsHeaderType.NFTMLN; + ProductFits.saveNftFits(fitsData, planeList, outfile, hdrBuilder, hdrType, crossrefFile); } - if (!qualityFile.isEmpty()) { - thisFile = Paths.get(qualityFile); - if (Files.exists(thisFile)) { - toolexe.append(" -quality-file "); - toolexe.append(thisFile.toAbsolutePath()); - } else { - logger.warn("Could not find quality file:{}", qualityFile); - } + private static void saveDTMFits( + FitsHdrBuilder hdrBuilder, + FitsData fitsData, + List planeList, + String namingConvention, + AltwgDataType productType, + boolean isGlobal, + String outfile) + throws FitsException, IOException { + + // Use a different static method to create the ALTWG product. This allows me to differentiate + // between different kinds of fits header types. + FitsHeaderType hdrType = FitsHeaderType.DTMLOCALALTWG; + + File crossrefFile; + String outFitsFname = outfile; + + // possible renaming of output file + NameConvention nameConvention = NameConvention.parseNameConvention(namingConvention); + File[] outFiles = + NamingFactory.getBaseNameAndCrossRef(nameConvention, hdrBuilder, productType, isGlobal, outfile); + + // check if cross-ref file is not null. If so then output file was renamed to naming convention. + crossrefFile = outFiles[1]; + if (crossrefFile != null) { + + // rename fitsFile per naming convention and create a cross-reference file. + String baseOutputName = outFiles[0].toString(); + + // determine whether original outfile is a directory + File outFname = new File(outfile); + String outputFolder = outFname.getAbsoluteFile().getParent(); + if (outFname.isDirectory()) { + outputFolder = outfile; + + // cannot create cross-reference file if original outfile was a directory. make it null; + crossrefFile = null; + } + outFitsFname = new File(outputFolder, baseOutputName + ".fits").getAbsolutePath(); + } + ProductFits.saveDataCubeFits(fitsData, planeList, outFitsFname, hdrBuilder, hdrType, crossrefFile); } - - toolexe.append(" "); - toolexe.append(mapletFile.getAbsolutePath()); - toolexe.append(" "); - toolexe.append(outFits); - - return toolexe.toString(); - } - - private static float getSigma(DataInput dataIn, double sigmaScale) throws IOException { - float floatVal = 0f; - if (dataIn != null) { - floatVal = dataIn.readFloat(); - } - floatVal = floatVal * (float) sigmaScale; - return floatVal; - } - - private static float getQuality(DataInput dataIn) throws IOException { - float floatVal = 0f; - if (dataIn != null) { - floatVal = dataIn.readFloat(); - } - return floatVal; - } - - private static void saveNFTFits( - FitsHdrBuilder hdrBuilder, - FitsData fitsData, - List planeList, - String namingConvention, - String outfile) - throws FitsException, IOException { - - File crossrefFile = null; - - NameConvention nameConvention = NameConvention.parseNameConvention(namingConvention); - if (nameConvention != NameConvention.NONEUSED) { - - String outNFTFname = outfile; - Path outPath = Paths.get(outNFTFname); - - // save old filename - String oldFilename = outPath.getFileName().toString(); - - // hardcode for now. NFT is not a nominal Toolkit product. - AltwgDataType productType = null; - boolean isGlobal = false; - - // generate new NFT output filename based on naming convention - ProductNamer productNamer = NamingFactory.parseNamingConvention(namingConvention); - String newbaseName = productNamer.productbaseName(hdrBuilder, productType, isGlobal); - - String newFilename = newbaseName + ".fits"; - - // replace outfile with new nft filename and write FITS file to it. - outNFTFname = outNFTFname.replace(oldFilename, newFilename); - - // save new PDS name in cross-reference file, for future reference - crossrefFile = new File(outNFTFname + ".crf"); - AsciiFile crfFile = new AsciiFile(crossrefFile.getAbsolutePath()); - crfFile.streamSToFile(newbaseName, 0); - crfFile.closeFile(); - - outfile = outNFTFname; - } - - FitsHeaderType hdrType = FitsHeaderType.NFTMLN; - ProductFits.saveNftFits(fitsData, planeList, outfile, hdrBuilder, hdrType, crossrefFile); - } - - private static void saveDTMFits( - FitsHdrBuilder hdrBuilder, - FitsData fitsData, - List planeList, - String namingConvention, - AltwgDataType productType, - boolean isGlobal, - String outfile) - throws FitsException, IOException { - - // Use a different static method to create the ALTWG product. This allows me to differentiate - // between different kinds of fits header types. - FitsHeaderType hdrType = FitsHeaderType.DTMLOCALALTWG; - - File crossrefFile; - String outFitsFname = outfile; - - // possible renaming of output file - NameConvention nameConvention = NameConvention.parseNameConvention(namingConvention); - File[] outFiles = - NamingFactory.getBaseNameAndCrossRef( - nameConvention, hdrBuilder, productType, isGlobal, outfile); - - // check if cross-ref file is not null. If so then output file was renamed to naming convention. - crossrefFile = outFiles[1]; - if (crossrefFile != null) { - - // rename fitsFile per naming convention and create a cross-reference file. - String baseOutputName = outFiles[0].toString(); - - // determine whether original outfile is a directory - File outFname = new File(outfile); - String outputFolder = outFname.getAbsoluteFile().getParent(); - if (outFname.isDirectory()) { - outputFolder = outfile; - - // cannot create cross-reference file if original outfile was a directory. make it null; - crossrefFile = null; - } - outFitsFname = new File(outputFolder, baseOutputName + ".fits").getAbsolutePath(); - } - ProductFits.saveDataCubeFits( - fitsData, planeList, outFitsFname, hdrBuilder, hdrType, crossrefFile); - } } diff --git a/src/main/java/terrasaur/apps/OBJ2DSK.java b/src/main/java/terrasaur/apps/OBJ2DSK.java index fe0d53c..e099bf5 100644 --- a/src/main/java/terrasaur/apps/OBJ2DSK.java +++ b/src/main/java/terrasaur/apps/OBJ2DSK.java @@ -22,6 +22,9 @@ */ package terrasaur.apps; +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; @@ -33,9 +36,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParameterException; import org.apache.commons.cli.Options; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -51,565 +51,611 @@ import vtk.vtkPolyData; public class OBJ2DSK implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - - - @Override - public String shortDescription() { - return "Convert an OBJ shape file to SPICE DSK format."; - } - - @Override - public String fullDescription(Options options) { - StringBuilder builder = new StringBuilder(); - Arguments arguments = new Arguments(); - JCommander jcommander = new JCommander(arguments); - jcommander.setProgramName("OBJ2DSK"); - - JCommanderUsage jcUsage = new JCommanderUsage(jcommander); - jcUsage.setColumnSize(100); - jcUsage.usage(builder, 4, arguments.commandDescription); - return builder.toString(); - - } - - private enum DSK_KEYS { - SURF_NAME, CENTER_NAME, REFFRAME_NAME, NAIF_SURFNAME, NAIF_SURFCODE, NAIF_SURFBODY, METAK, COMMENTFILE - } - - private static class Arguments { - - private final String commandDescription = AppVersion.getVersionString() - + "\n\nConverts a triangular plate model in OBJ format to a SPICE DSK file.\n" - + "The SPICE utility application 'mkdsk' must already be present on your PATH.\n"; - - @Parameter(names = "-help", help = true) - private boolean help; - - @Parameter(names = "--latMin", description = " Minimum latitude of OBJ in degrees.", - required = false, order = 0) - private double latMin = -90D; - - @Parameter(names = "--latMax", description = " Maximum latitude of OBJ in degrees.", - required = false, order = 1) - private double latMax = 90D; - - @Parameter(names = "--lonMin", description = " Minimum longitude of OBJ in degrees.", - required = false, order = 2) - private double lonMin = 0; - - @Parameter(names = "--lonMax", description = " Maximum longitude of OBJ in degrees.", - required = false, order = 3) - private double lonMax = 360D; - - @Parameter(names = "--fitsFile", - description = " path to DTM fits file containing lat,lon" - + " information as planes. Assumes PLANE1=latitude, PLANE2=longitude. Use in place of specifying lat/lon min/max values.", - required = false, order = 4) - private String fitsFile = ""; - - @Parameter(names = "--fine-scale", - description = " Floating point value representing the " - + " ratio of the spatial index's fine voxel edge length to the average plate extent. " - + " The 'extent' of a plate in a given coordinate direction is the difference between the maximum and minimum " - + " values of that coordinate attained on the plate. Only required if mkdsk version is " - + " lower than 66.", - required = false, order = 11) - double fineVoxScale = Double.NaN; - - @Parameter(names = "--coarse-scale", - description = " " - + " Integer value representing the ratio of the edge length of coarse voxels to" - + " fine voxels. The number must be large enough that the total number of coarse" - + " voxels is less than the value of MAXCGR, which is currently 1E5." - + " Only required if mkdsk version is lower than 66.", - required = false, order = 12) - Integer coarseVoxScale = -999; - - @Parameter(names = "--useSetupFile", - description = " Use " - + " instead of the default setup file created by the tool.", - required = false, order = 13) - String inputSetup = ""; - - @Parameter(names = "--writesetupFile", - description = " Write the setup file" - + " to the specified path instead of writing it as a temporary file which gets deleted" - + " after execution.", - required = false, order = 14) - String outsetupFname = ""; - - @Parameter(names = "--keepTempFiles", - description = "enable this to prevent setup files" - + " from being deleted. Used for debugging purposes to see what is being sent" - + " to mkdsk.", - required = false, order = 15) - boolean keepTmpFiles = false; - - @Parameter(names = "--mkFile", - description = " path to SPICE meta kernel file." - + " Metakernel only needs to point to leap seconds kernel and a frames kernel that contains" - + " the digital ID to CENTER_NAME and REF_FRAME_NAME lookup table." - + " This argument is REQUIRED if user does NOT supply a setupFile!", - required = false, order = 4) - String mkFile = ""; - - @Parameter(names = "--surfName", - description = " Allows user to modify the " - + " SURFACE_NAME (name of the specific shape data set for the central body)" - + " used in the default setup file created by the tool. This is a required" - + " keyword in the setup file.", - required = false, order = 5) - String surfaceName = "BENNU"; - - @Parameter(names = "--centName", description = " Allows user to modify the " - + " CENTER_NAME (central body name) used in the default setup file created by the tool. " - + " Can also be an ID code. This is a required keyword in the setup file.", - required = false, order = 6) - String centerName = "BENNU"; - - @Parameter(names = "--refName", description = " Allows user to modify the " - + " REF_FRAME_NAME (reference frame name) used in the default setup file created by the tool. " - + " This is a required keyword in the setup file.", required = false, order = 7) - String refFrameName = "IAU_BENNU"; - - @Parameter(names = "--naif_surfName", - description = " Allows user to add the " - + " NAIF_SURFACE_NAME to the default setup file created by the tool. " - + " This may be needed under certain conditions. Optional keyword. " - + " Default is not to use it.", - required = false, order = 8) - String naifSurfName = ""; - - @Parameter(names = "--naif_surfCode", - description = " Allows user to add the " - + " NAIF_SURFACE_CODE to the default setup file created by the tool. " - + " Allows the tool to associate this ID code to the NAIF_SURFACE_NAME. Optional keyword. " - + " Default is not to use it.", - required = false, order = 9) - String naifSurfCode = ""; - - @Parameter(names = "--naif_surfBody", - description = " Allows user to add the " - + " NAIF_SURFACE_BODY to the default setup file created by the tool. " - + " This may be needed under certain conditions. Optional keyword." - + " Default is not to use it.", - required = false, order = 10) - String naifSurfBody = ""; - - @Parameter(names = "--cmtFile", - description = " Specify the comment file" - + " that mkdsk will add to the DSK. Comment file is an ASCII file containing" - + " additional information about the DSK. Default is single space.", - required = false, order = 11) - String cmtFile = " "; - - @Parameter(names = "-shortDescription", hidden = true) - private boolean shortDescription = false; - - @Parameter( - description = " Versions of mkdsk that are V066 and higher will automatically calculate the\n" - + " voxel scales which optimize processing time without exceeding maximum array sizes.\n" - + " However, if you are using a version of mkdsk that is below v066 you can specify the\n" - + " FINE_VOXEL_SCALE and COARSE_VOXEL_SCALE via the '--fine-scale' and '--coarse-scale'\n" - + " optional arguments.\n" - + " Run 'mkdsk' by itself and note the 'Toolkit ver' version number. If it is below\n" - + " 066 then you MUST specify the fine and coarse voxel scales.\n" - + " See SPICE documentation notes on recommended values.\n\n" - + "Usage: OBJ2DSK [options] \nWhere:\n" - + " \n" + " input OBJ file\n" + " \n" - + " output dsk file\n" - + " NOTE: MUST set --metakFile if not supplying a custom setup file!") - private List files = new ArrayList<>(); - - } - - private final Double fineVoxScale; - private final Integer coarseVoxScale; - - public OBJ2DSK(double fineVoxScale, int coarseVoxScale) { - this.fineVoxScale = fineVoxScale; - this.coarseVoxScale = coarseVoxScale; - } - - public OBJ2DSK() { - this(Double.NaN, -1); - } - - public static void main(String[] args) throws Exception { - - TerrasaurTool defaultObj = new OBJ2DSK(); - - int i = 0; - Map latLonMinMax = new HashMap(); - // String fitsFname = ""; - // String temp; - // String inputSetup = ""; - // boolean keepTmpFiles = false; - - // check for -shortDescription before looking for required arguments - for (String arg : args) { - if (arg.equals("-shortDescription")) { - System.out.println(defaultObj.shortDescription()); - System.exit(0); - } + @Override + public String shortDescription() { + return "Convert an OBJ shape file to SPICE DSK format."; } - Arguments arg = new Arguments(); + @Override + public String fullDescription(Options options) { + StringBuilder builder = new StringBuilder(); + Arguments arguments = new Arguments(); + JCommander jcommander = new JCommander(arguments); + jcommander.setProgramName("OBJ2DSK"); - JCommander command = new JCommander(arg); - try { - // @Deprecated - // command = new JCommander(arg, args); - command.parse(args); - } catch (ParameterException ex) { - System.out.println(defaultObj.fullDescription(null)); - System.out.println("Error parsing input arguments:"); - System.out.println(ex.getMessage()); - command = new JCommander(arg); - System.exit(1); + JCommanderUsage jcUsage = new JCommanderUsage(jcommander); + jcUsage.setColumnSize(100); + jcUsage.usage(builder, 4, arguments.commandDescription); + return builder.toString(); + } + + private enum DSK_KEYS { + SURF_NAME, + CENTER_NAME, + REFFRAME_NAME, + NAIF_SURFNAME, + NAIF_SURFCODE, + NAIF_SURFBODY, + METAK, + COMMENTFILE + } + + private static class Arguments { + + private final String commandDescription = AppVersion.getVersionString() + + "\n\nConverts a triangular plate model in OBJ format to a SPICE DSK file.\n" + + "The SPICE utility application 'mkdsk' must already be present on your PATH.\n"; + + @Parameter(names = "-help", help = true) + private boolean help; + + @Parameter( + names = "--latMin", + description = " Minimum latitude of OBJ in degrees.", + required = false, + order = 0) + private double latMin = -90D; + + @Parameter( + names = "--latMax", + description = " Maximum latitude of OBJ in degrees.", + required = false, + order = 1) + private double latMax = 90D; + + @Parameter( + names = "--lonMin", + description = " Minimum longitude of OBJ in degrees.", + required = false, + order = 2) + private double lonMin = 0; + + @Parameter( + names = "--lonMax", + description = " Maximum longitude of OBJ in degrees.", + required = false, + order = 3) + private double lonMax = 360D; + + @Parameter( + names = "--fitsFile", + description = + " path to DTM fits file containing lat,lon" + + " information as planes. Assumes PLANE1=latitude, PLANE2=longitude. Use in place of specifying lat/lon min/max values.", + required = false, + order = 4) + private String fitsFile = ""; + + @Parameter( + names = "--fine-scale", + description = " Floating point value representing the " + + " ratio of the spatial index's fine voxel edge length to the average plate extent. " + + " The 'extent' of a plate in a given coordinate direction is the difference between the maximum and minimum " + + " values of that coordinate attained on the plate. Only required if mkdsk version is " + + " lower than 66.", + required = false, + order = 11) + double fineVoxScale = Double.NaN; + + @Parameter( + names = "--coarse-scale", + description = " " + + " Integer value representing the ratio of the edge length of coarse voxels to" + + " fine voxels. The number must be large enough that the total number of coarse" + + " voxels is less than the value of MAXCGR, which is currently 1E5." + + " Only required if mkdsk version is lower than 66.", + required = false, + order = 12) + Integer coarseVoxScale = -999; + + @Parameter( + names = "--useSetupFile", + description = " Use " + + " instead of the default setup file created by the tool.", + required = false, + order = 13) + String inputSetup = ""; + + @Parameter( + names = "--writesetupFile", + description = " Write the setup file" + + " to the specified path instead of writing it as a temporary file which gets deleted" + + " after execution.", + required = false, + order = 14) + String outsetupFname = ""; + + @Parameter( + names = "--keepTempFiles", + description = "enable this to prevent setup files" + + " from being deleted. Used for debugging purposes to see what is being sent" + + " to mkdsk.", + required = false, + order = 15) + boolean keepTmpFiles = false; + + @Parameter( + names = "--mkFile", + description = " path to SPICE meta kernel file." + + " Metakernel only needs to point to leap seconds kernel and a frames kernel that contains" + + " the digital ID to CENTER_NAME and REF_FRAME_NAME lookup table." + + " This argument is REQUIRED if user does NOT supply a setupFile!", + required = false, + order = 4) + String mkFile = ""; + + @Parameter( + names = "--surfName", + description = " Allows user to modify the " + + " SURFACE_NAME (name of the specific shape data set for the central body)" + + " used in the default setup file created by the tool. This is a required" + + " keyword in the setup file.", + required = false, + order = 5) + String surfaceName = "BENNU"; + + @Parameter( + names = "--centName", + description = " Allows user to modify the " + + " CENTER_NAME (central body name) used in the default setup file created by the tool. " + + " Can also be an ID code. This is a required keyword in the setup file.", + required = false, + order = 6) + String centerName = "BENNU"; + + @Parameter( + names = "--refName", + description = " Allows user to modify the " + + " REF_FRAME_NAME (reference frame name) used in the default setup file created by the tool. " + + " This is a required keyword in the setup file.", + required = false, + order = 7) + String refFrameName = "IAU_BENNU"; + + @Parameter( + names = "--naif_surfName", + description = " Allows user to add the " + + " NAIF_SURFACE_NAME to the default setup file created by the tool. " + + " This may be needed under certain conditions. Optional keyword. " + + " Default is not to use it.", + required = false, + order = 8) + String naifSurfName = ""; + + @Parameter( + names = "--naif_surfCode", + description = " Allows user to add the " + + " NAIF_SURFACE_CODE to the default setup file created by the tool. " + + " Allows the tool to associate this ID code to the NAIF_SURFACE_NAME. Optional keyword. " + + " Default is not to use it.", + required = false, + order = 9) + String naifSurfCode = ""; + + @Parameter( + names = "--naif_surfBody", + description = " Allows user to add the " + + " NAIF_SURFACE_BODY to the default setup file created by the tool. " + + " This may be needed under certain conditions. Optional keyword." + + " Default is not to use it.", + required = false, + order = 10) + String naifSurfBody = ""; + + @Parameter( + names = "--cmtFile", + description = " Specify the comment file" + + " that mkdsk will add to the DSK. Comment file is an ASCII file containing" + + " additional information about the DSK. Default is single space.", + required = false, + order = 11) + String cmtFile = " "; + + @Parameter(names = "-shortDescription", hidden = true) + private boolean shortDescription = false; + + @Parameter( + description = " Versions of mkdsk that are V066 and higher will automatically calculate the\n" + + " voxel scales which optimize processing time without exceeding maximum array sizes.\n" + + " However, if you are using a version of mkdsk that is below v066 you can specify the\n" + + " FINE_VOXEL_SCALE and COARSE_VOXEL_SCALE via the '--fine-scale' and '--coarse-scale'\n" + + " optional arguments.\n" + + " Run 'mkdsk' by itself and note the 'Toolkit ver' version number. If it is below\n" + + " 066 then you MUST specify the fine and coarse voxel scales.\n" + + " See SPICE documentation notes on recommended values.\n\n" + + "Usage: OBJ2DSK [options] \nWhere:\n" + + " \n" + " input OBJ file\n" + " \n" + + " output dsk file\n" + + " NOTE: MUST set --metakFile if not supplying a custom setup file!") + private List files = new ArrayList<>(); + } + + private final Double fineVoxScale; + private final Integer coarseVoxScale; + + public OBJ2DSK(double fineVoxScale, int coarseVoxScale) { + this.fineVoxScale = fineVoxScale; + this.coarseVoxScale = coarseVoxScale; + } + + public OBJ2DSK() { + this(Double.NaN, -1); + } + + public static void main(String[] args) throws Exception { + + TerrasaurTool defaultObj = new OBJ2DSK(); + + int i = 0; + Map latLonMinMax = new HashMap(); + // String fitsFname = ""; + // String temp; + // String inputSetup = ""; + // boolean keepTmpFiles = false; + + // check for -shortDescription before looking for required arguments + for (String arg : args) { + if (arg.equals("-shortDescription")) { + System.out.println(defaultObj.shortDescription()); + System.exit(0); + } + } + + Arguments arg = new Arguments(); + + JCommander command = new JCommander(arg); + try { + // @Deprecated + // command = new JCommander(arg, args); + command.parse(args); + } catch (ParameterException ex) { + System.out.println(defaultObj.fullDescription(null)); + System.out.println("Error parsing input arguments:"); + System.out.println(ex.getMessage()); + command = new JCommander(arg); + System.exit(1); + } + + if ((args.length < 1) || (arg.help)) { + System.out.println(defaultObj.fullDescription(null)); + System.exit(0); + } + + // This is to avoid java crashing due to inability to connect to an X display + System.setProperty("java.awt.headless", "true"); + + String spiceFile = ""; + String inFile = ""; + String outFile = ""; + if (arg.files.size() != 2) { + String errMesg = "ERROR! Expecting 2 required inputs: input OBJ, output DSK"; + throw new RuntimeException(errMesg); + } else { + spiceFile = arg.mkFile; + inFile = arg.files.get(0); + outFile = arg.files.get(1); + } + + boolean useFitsFile = false; + boolean latLonSet = false; + + String fitsFname = arg.fitsFile; + if (!fitsFname.isEmpty()) { + useFitsFile = true; + System.out.println("Will use lat,lons from " + fitsFname + " to set lat,lon bounds."); + // load the fits header and parse for min, max lat, lon values + latLonMinMax = ProductFits.minMaxLLFromFits(new File(fitsFname)); + + if (latLonMinMax.size() < 4) { + System.out.println("ERROR! Could not parse all min,max lat/lon corners!"); + System.out.println("Unable to create DSK for " + inFile + "!"); + System.exit(1); + } + } else { + // parse lat,lon bounds. Some of these may be set to default values. + latLonMinMax.put(HeaderTag.MINLAT.toString(), arg.latMin); + latLonMinMax.put(HeaderTag.MAXLAT.toString(), arg.latMax); + latLonMinMax.put(HeaderTag.MINLON.toString(), arg.lonMin); + latLonMinMax.put(HeaderTag.MAXLON.toString(), arg.lonMax); + latLonSet = true; + } + + System.out.println("Using these lat, lon bounds:"); + for (String thisKey : latLonMinMax.keySet()) { + System.out.println("key:" + thisKey + ", value:" + latLonMinMax.get(thisKey)); + } + + NativeLibraryLoader.loadVtkLibraries(); + + OBJ2DSK obj2dsk; + if ((Double.isNaN(arg.fineVoxScale)) || (arg.coarseVoxScale < 0)) { + obj2dsk = new OBJ2DSK(); + } else { + obj2dsk = new OBJ2DSK(arg.fineVoxScale, arg.coarseVoxScale); + } + + // generate setup file if needed + File setupFile = null; + Map dskParams = new HashMap(); + dskParams.put(DSK_KEYS.SURF_NAME, arg.surfaceName); + dskParams.put(DSK_KEYS.CENTER_NAME, arg.centerName); + dskParams.put(DSK_KEYS.REFFRAME_NAME, arg.refFrameName); + dskParams.put(DSK_KEYS.NAIF_SURFNAME, arg.naifSurfName); + dskParams.put(DSK_KEYS.NAIF_SURFCODE, arg.naifSurfCode); + dskParams.put(DSK_KEYS.NAIF_SURFBODY, arg.naifSurfBody); + dskParams.put(DSK_KEYS.COMMENTFILE, arg.cmtFile); + dskParams.put(DSK_KEYS.METAK, spiceFile); + + String outsetupFname = arg.outsetupFname; + String inputSetup = arg.inputSetup; + + boolean keepTmpFiles = arg.keepTmpFiles; + if (inputSetup.isEmpty()) { + + if (spiceFile.isEmpty()) { + String errMesg = "ERROR! MUST supply path to SPICE metakernel via --mkFile!"; + throw new RuntimeException(errMesg); + } + + System.out.println("Creating default setup file"); + setupFile = + createSetup(latLonMinMax, obj2dsk.fineVoxScale, obj2dsk.coarseVoxScale, dskParams, outsetupFname); + if (keepTmpFiles) { + System.out.println("setup file created here:" + outsetupFname); + } else { + setupFile.deleteOnExit(); + } + + } else { + + // check that input setup file exists + setupFile = new File(inputSetup); + if (!setupFile.exists()) { + String errMesg = "Custom setup file:" + inputSetup + " not found!"; + throw new RuntimeException(errMesg); + } + System.out.println("Using custom setup file:" + inputSetup); + } + obj2dsk.run(inFile, outFile, latLonMinMax, setupFile, outsetupFname, keepTmpFiles); + + /* + * for (; i < args.length; ++i) { if (args[i].equals("-shortDescription")) { + * System.out.println(defaultObj.shortDescription()); System.exit(0); } else if + * (args[i].equals("--latMin")) { latLonMinMax.put(HeaderTag.MINLAT.toString(), + * StringUtil.parseSafeD(args[++i])); latLonSet = true; } else if (args[i].equals("--latMax")) { + * latLonMinMax.put(HeaderTag.MAXLAT.toString(), StringUtil.parseSafeD(args[++i])); latLonSet = + * true; } else if (args[i].equals("--lonMin")) { latLonMinMax.put(HeaderTag.MINLON.toString(), + * StringUtil.parseSafeD(args[++i])); latLonSet = true; } else if (args[i].equals("--lonMax")) { + * latLonMinMax.put(HeaderTag.MAXLON.toString(), StringUtil.parseSafeD(args[++i])); latLonSet = + * true; } else if (args[i].equals("--fits-file")) { fitsFname = args[++i]; useFitsFile = true; + * } else if (args[i].equals("--fine-scale")) { temp = args[++i]; fineVoxScale = + * StringUtil.parseSafeD(temp); if (fineVoxScale == Double.NaN) { + * System.err.println("ERROR! value for fine-scale:" + temp); + * System.err.println("Could not be converted into a double!"); + * System.out.println("Exiting program."); System.exit(1); } userVox = true; + * + * } else if (args[i].equals("--coarse-scale")) { + * + * temp = args[++i]; double tempD = StringUtil.parseSafeD(temp); if (tempD == Double.NaN) { + * System.err.println("ERROR! value for coarse-scale:" + temp); + * System.err.println("Could not be converted into a number!"); + * System.out.println("Exiting program."); System.exit(1); } coarseVoxScale = (int) tempD; + * userVox = true; + * + * } else if (args[i].equals("--writesetupFile")) { outsetupFname = args[++i]; } else if + * (args[i].equals("--useSetupFile")) { inputSetup = args[++i]; keepTmpFiles = true; } else if + * (args[i].equals("--keepTempFiles")) { keepTmpFiles = true; } else { break; } } + * + * // There must be numRequiredArgs arguments remaining after the options. // Otherwise abort. + * int numberRequiredArgs = 3; if (args.length - i != numberRequiredArgs) { + * System.out.println(defaultObj.fullDescription()); System.exit(0); } + * + * String current = new java.io.File(".").getAbsolutePath(); + * System.out.println("Running OBJ2DSK. Current dir:" + current); + * + * String spicefile = args[i++]; String infile = args[i++]; String outfile = args[i++]; + */ } - if ((args.length < 1) || (arg.help)) { - System.out.println(defaultObj.fullDescription(null)); - System.exit(0); + public void run( + String infile, + String outfile, + Map latLonMinMax, + File setupFile, + String outsetupFname, + boolean keepTmpFiles) + throws Exception { + + System.out.println("Running OBJ2DSK."); + System.out.println("FINE_VOXEL_SCALE = " + Double.toString(fineVoxScale)); + System.out.println("COARSE_VOXEL_SCALE = " + Integer.toString(coarseVoxScale)); + + // File setupFile = null; + // if (inputSetup.length() < 1) { + // System.out.println("Creating default setup file"); + // setupFile = createSetup(spicefile, latLonMinMax, this.fineVoxScale, this.coarseVoxScale, + // outsetupFname); + // if (keepTmpFiles) { + // System.out.println("setup file created here:" + outsetupFname); + // } else { + // setupFile.deleteOnExit(); + // } + // } else { + // // check that input setup file exists + // setupFile = new File(inputSetup); + // if (!setupFile.exists()) { + // String errMesg = "Custom setup file:" + inputSetup + " not found!"; + // throw new RuntimeException(errMesg); + // } + // System.out.println("Using custom setup file:" + inputSetup); + // } + + vtkPolyData inpolydata = PolyDataUtil.loadShapeModelAndComputeNormals(infile); + + // We need to save out the OBJ file again in case it contains comment + // lines since mkdsk does not support lines beginning with # + // The OBJ file is saved to a temporary filename in order to preserve the + // original OBJ. The temporary filename is deleted afterwards. + File shapeModel = File.createTempFile("shapemodel-", null); + shapeModel.deleteOnExit(); + PolyDataUtil.saveShapeModelAsOBJ(inpolydata, shapeModel.getAbsolutePath()); + + // Delete dsk file if already exists since otherwise mkdsk will complain + if (new File(outfile).isFile()) new File(outfile).delete(); + + String command = "mkdsk -setup " + setupFile.getAbsolutePath() + " -input " + shapeModel.getAbsolutePath() + + " -output " + outfile; + ProcessUtils.runProgramAndWait(command, null, false); } - // This is to avoid java crashing due to inability to connect to an X display - System.setProperty("java.awt.headless", "true"); - - String spiceFile = ""; - String inFile = ""; - String outFile = ""; - if (arg.files.size() != 2) { - String errMesg = "ERROR! Expecting 2 required inputs: input OBJ, output DSK"; - throw new RuntimeException(errMesg); - } else { - spiceFile = arg.mkFile; - inFile = arg.files.get(0); - outFile = arg.files.get(1); - } - - boolean useFitsFile = false; - boolean latLonSet = false; - - String fitsFname = arg.fitsFile; - if (!fitsFname.isEmpty()) { - useFitsFile = true; - System.out.println("Will use lat,lons from " + fitsFname + " to set lat,lon bounds."); - // load the fits header and parse for min, max lat, lon values - latLonMinMax = ProductFits.minMaxLLFromFits(new File(fitsFname)); - - if (latLonMinMax.size() < 4) { - System.out.println("ERROR! Could not parse all min,max lat/lon corners!"); - System.out.println("Unable to create DSK for " + inFile + "!"); - System.exit(1); - } - } else { - // parse lat,lon bounds. Some of these may be set to default values. - latLonMinMax.put(HeaderTag.MINLAT.toString(), arg.latMin); - latLonMinMax.put(HeaderTag.MAXLAT.toString(), arg.latMax); - latLonMinMax.put(HeaderTag.MINLON.toString(), arg.lonMin); - latLonMinMax.put(HeaderTag.MAXLON.toString(), arg.lonMax); - latLonSet = true; - } - - System.out.println("Using these lat, lon bounds:"); - for (String thisKey : latLonMinMax.keySet()) { - System.out.println("key:" + thisKey + ", value:" + latLonMinMax.get(thisKey)); - } - - NativeLibraryLoader.loadVtkLibraries(); - - - OBJ2DSK obj2dsk; - if ((Double.isNaN(arg.fineVoxScale)) || (arg.coarseVoxScale < 0)) { - obj2dsk = new OBJ2DSK(); - } else { - obj2dsk = new OBJ2DSK(arg.fineVoxScale, arg.coarseVoxScale); - } - - // generate setup file if needed - File setupFile = null; - Map dskParams = new HashMap(); - dskParams.put(DSK_KEYS.SURF_NAME, arg.surfaceName); - dskParams.put(DSK_KEYS.CENTER_NAME, arg.centerName); - dskParams.put(DSK_KEYS.REFFRAME_NAME, arg.refFrameName); - dskParams.put(DSK_KEYS.NAIF_SURFNAME, arg.naifSurfName); - dskParams.put(DSK_KEYS.NAIF_SURFCODE, arg.naifSurfCode); - dskParams.put(DSK_KEYS.NAIF_SURFBODY, arg.naifSurfBody); - dskParams.put(DSK_KEYS.COMMENTFILE, arg.cmtFile); - dskParams.put(DSK_KEYS.METAK, spiceFile); - - String outsetupFname = arg.outsetupFname; - String inputSetup = arg.inputSetup; - - boolean keepTmpFiles = arg.keepTmpFiles; - if (inputSetup.isEmpty()) { - - if (spiceFile.isEmpty()) { - String errMesg = "ERROR! MUST supply path to SPICE metakernel via --mkFile!"; - throw new RuntimeException(errMesg); - } - - System.out.println("Creating default setup file"); - setupFile = createSetup(latLonMinMax, obj2dsk.fineVoxScale, obj2dsk.coarseVoxScale, dskParams, - outsetupFname); - if (keepTmpFiles) { - System.out.println("setup file created here:" + outsetupFname); - } else { - setupFile.deleteOnExit(); - } - - } else { - - // check that input setup file exists - setupFile = new File(inputSetup); - if (!setupFile.exists()) { - String errMesg = "Custom setup file:" + inputSetup + " not found!"; - throw new RuntimeException(errMesg); - } - System.out.println("Using custom setup file:" + inputSetup); - - } - obj2dsk.run(inFile, outFile, latLonMinMax, setupFile, outsetupFname, keepTmpFiles); - - /* - * for (; i < args.length; ++i) { if (args[i].equals("-shortDescription")) { - * System.out.println(defaultObj.shortDescription()); System.exit(0); } else if - * (args[i].equals("--latMin")) { latLonMinMax.put(HeaderTag.MINLAT.toString(), - * StringUtil.parseSafeD(args[++i])); latLonSet = true; } else if (args[i].equals("--latMax")) { - * latLonMinMax.put(HeaderTag.MAXLAT.toString(), StringUtil.parseSafeD(args[++i])); latLonSet = - * true; } else if (args[i].equals("--lonMin")) { latLonMinMax.put(HeaderTag.MINLON.toString(), - * StringUtil.parseSafeD(args[++i])); latLonSet = true; } else if (args[i].equals("--lonMax")) { - * latLonMinMax.put(HeaderTag.MAXLON.toString(), StringUtil.parseSafeD(args[++i])); latLonSet = - * true; } else if (args[i].equals("--fits-file")) { fitsFname = args[++i]; useFitsFile = true; - * } else if (args[i].equals("--fine-scale")) { temp = args[++i]; fineVoxScale = - * StringUtil.parseSafeD(temp); if (fineVoxScale == Double.NaN) { - * System.err.println("ERROR! value for fine-scale:" + temp); - * System.err.println("Could not be converted into a double!"); - * System.out.println("Exiting program."); System.exit(1); } userVox = true; - * - * } else if (args[i].equals("--coarse-scale")) { - * - * temp = args[++i]; double tempD = StringUtil.parseSafeD(temp); if (tempD == Double.NaN) { - * System.err.println("ERROR! value for coarse-scale:" + temp); - * System.err.println("Could not be converted into a number!"); - * System.out.println("Exiting program."); System.exit(1); } coarseVoxScale = (int) tempD; - * userVox = true; - * - * } else if (args[i].equals("--writesetupFile")) { outsetupFname = args[++i]; } else if - * (args[i].equals("--useSetupFile")) { inputSetup = args[++i]; keepTmpFiles = true; } else if - * (args[i].equals("--keepTempFiles")) { keepTmpFiles = true; } else { break; } } - * - * // There must be numRequiredArgs arguments remaining after the options. // Otherwise abort. - * int numberRequiredArgs = 3; if (args.length - i != numberRequiredArgs) { - * System.out.println(defaultObj.fullDescription()); System.exit(0); } - * - * String current = new java.io.File(".").getAbsolutePath(); - * System.out.println("Running OBJ2DSK. Current dir:" + current); - * - * String spicefile = args[i++]; String infile = args[i++]; String outfile = args[i++]; + /** + * Create the setup file for mkdsk executable. + * + * @param latLonCorners + * @param fineVoxScale + * @param coarseVoxScale + * @param dskParams + * @param setupFname + * @return */ + private static File createSetup( + Map latLonCorners, + Double fineVoxScale, + Integer coarseVoxScale, + Map dskParams, + String setupFname) { - } + // evaluate latlon corners. Exit program if any are NaN. + evaluateCorners(latLonCorners); - public void run(String infile, String outfile, Map latLonMinMax, File setupFile, - String outsetupFname, boolean keepTmpFiles) throws Exception { + File setupFile; - System.out.println("Running OBJ2DSK."); - System.out.println("FINE_VOXEL_SCALE = " + Double.toString(fineVoxScale)); - System.out.println("COARSE_VOXEL_SCALE = " + Integer.toString(coarseVoxScale)); + if (setupFname.length() < 1) { + setupFile = null; + try { + setupFile = File.createTempFile("setupfile-", null); + } catch (IOException e) { + String errMesg = "ERROR creating setupfile:" + setupFname; + throw new RuntimeException(errMesg); + } + } else { + setupFile = new File(setupFname); + } + System.out.println("Setup file for mkdsk created here:" + + setupFile.getAbsolutePath().toString()); - // File setupFile = null; - // if (inputSetup.length() < 1) { - // System.out.println("Creating default setup file"); - // setupFile = createSetup(spicefile, latLonMinMax, this.fineVoxScale, this.coarseVoxScale, - // outsetupFname); - // if (keepTmpFiles) { - // System.out.println("setup file created here:" + outsetupFname); - // } else { - // setupFile.deleteOnExit(); - // } - // } else { - // // check that input setup file exists - // setupFile = new File(inputSetup); - // if (!setupFile.exists()) { - // String errMesg = "Custom setup file:" + inputSetup + " not found!"; - // throw new RuntimeException(errMesg); - // } - // System.out.println("Using custom setup file:" + inputSetup); - // } + // relativize the path to the metakernel file. Do this because mkdsk has a limit on the string + // length to the metakernel. Get normalized absolute path to mkFile in case the user enters a + // relative path string, e.x. ../../SPICE/spice-kernels.mk + Path currDir = FileSystems.getDefault().getPath("").toAbsolutePath(); + Path mkFile = Paths.get(dskParams.get(DSK_KEYS.METAK)).toAbsolutePath().normalize(); - vtkPolyData inpolydata = PolyDataUtil.loadShapeModelAndComputeNormals(infile); + System.out.println("currDir:" + currDir.toString()); + System.out.println("mkFile:" + mkFile.toString()); - // We need to save out the OBJ file again in case it contains comment - // lines since mkdsk does not support lines beginning with # - // The OBJ file is saved to a temporary filename in order to preserve the - // original OBJ. The temporary filename is deleted afterwards. - File shapeModel = File.createTempFile("shapemodel-", null); - shapeModel.deleteOnExit(); - PolyDataUtil.saveShapeModelAsOBJ(inpolydata, shapeModel.getAbsolutePath()); + // mkFile path relative to currDir + Path relPath = currDir.relativize(mkFile); + String spicefile = relPath.toString(); - // Delete dsk file if already exists since otherwise mkdsk will complain - if (new File(outfile).isFile()) - new File(outfile).delete(); + if (spicefile.length() > 80) { + System.out.println( + "Error: pointer to SPICE metakernel kernel file may not be longer than" + " 80 characters."); + System.out.println("The paths inside the metakernel file can be as long as 255 characters."); + System.exit(1); + } - String command = "mkdsk -setup " + setupFile.getAbsolutePath() + " -input " - + shapeModel.getAbsolutePath() + " -output " + outfile; - ProcessUtils.runProgramAndWait(command, null, false); - } + // create the content of setup file + StringBuilder sb = new StringBuilder(); + sb.append("\\begindata\n"); + sb.append("COMMENT_FILE = '" + dskParams.get(DSK_KEYS.COMMENTFILE) + "'\n"); + sb.append("LEAPSECONDS_FILE = '" + spicefile + "'\n"); + sb.append("SURFACE_NAME = '" + dskParams.get(DSK_KEYS.SURF_NAME) + "'\n"); + sb.append("CENTER_NAME = '" + dskParams.get(DSK_KEYS.CENTER_NAME) + "'\n"); + sb.append("REF_FRAME_NAME = '" + dskParams.get(DSK_KEYS.REFFRAME_NAME) + "'\n"); + // sb.append("SURFACE_NAME = 'BENNU'\n"); + // sb.append("CENTER_NAME = 'BENNU'\n"); + // sb.append("REF_FRAME_NAME = 'IAU_BENNU'\n"); + sb.append("START_TIME = '1950-JAN-1/00:00:00'\n"); + sb.append("STOP_TIME = '2050-JAN-1/00:00:00'\n"); + sb.append("DATA_CLASS = 2\n"); + sb.append("INPUT_DATA_UNITS = ( 'ANGLES = DEGREES'\n"); + sb.append(" 'DISTANCES = KILOMETERS' )\n"); + sb.append("COORDINATE_SYSTEM = 'LATITUDINAL'\n"); + String valueString = + String.format("MINIMUM_LATITUDE = %.5f\n", latLonCorners.get(HeaderTag.MINLAT.toString())); + sb.append(valueString); + // out.write("MAXIMUM_LATITUDE = 90\n"); + valueString = String.format("MAXIMUM_LATITUDE = %.5f\n", latLonCorners.get(HeaderTag.MAXLAT.toString())); + sb.append(valueString); - /** - * Create the setup file for mkdsk executable. - * - * @param latLonCorners - * @param fineVoxScale - * @param coarseVoxScale - * @param dskParams - * @param setupFname - * @return - */ - private static File createSetup(Map latLonCorners, Double fineVoxScale, - Integer coarseVoxScale, Map dskParams, String setupFname) { + // out.write("MINIMUM_LONGITUDE = -180\n"); + valueString = String.format("MINIMUM_LONGITUDE = %.5f\n", latLonCorners.get(HeaderTag.MINLON.toString())); + sb.append(valueString); - // evaluate latlon corners. Exit program if any are NaN. - evaluateCorners(latLonCorners); + // out.write("MAXIMUM_LONGITUDE = 180\n"); + valueString = String.format("MAXIMUM_LONGITUDE = %.5f\n", latLonCorners.get(HeaderTag.MAXLON.toString())); + sb.append(valueString); - File setupFile; + sb.append("DATA_TYPE = 2\n"); + sb.append("PLATE_TYPE = 3\n"); - if (setupFname.length() < 1) { - setupFile = null; - try { - setupFile = File.createTempFile("setupfile-", null); - } catch (IOException e) { - String errMesg = "ERROR creating setupfile:" + setupFname; - throw new RuntimeException(errMesg); - } - } else { - setupFile = new File(setupFname); - } - System.out - .println("Setup file for mkdsk created here:" + setupFile.getAbsolutePath().toString()); + String val; + if (fineVoxScale > 0D) { + val = fineVoxScale.toString(); + val = val.trim(); + sb.append("FINE_VOXEL_SCALE = " + val + "\n"); + } + if (coarseVoxScale > 0) { + val = coarseVoxScale.toString(); + val = val.trim(); + sb.append("COARSE_VOXEL_SCALE = " + val + "\n"); + } - // relativize the path to the metakernel file. Do this because mkdsk has a limit on the string - // length to the metakernel. Get normalized absolute path to mkFile in case the user enters a - // relative path string, e.x. ../../SPICE/spice-kernels.mk - Path currDir = FileSystems.getDefault().getPath("").toAbsolutePath(); - Path mkFile = Paths.get(dskParams.get(DSK_KEYS.METAK)).toAbsolutePath().normalize(); + String naifSurf = dskParams.get(DSK_KEYS.NAIF_SURFNAME); + String naifCode = dskParams.get(DSK_KEYS.NAIF_SURFCODE); + String naifBody = dskParams.get(DSK_KEYS.NAIF_SURFBODY); + if ((naifSurf.length() > 0) && (naifCode.length() > 0) && (naifBody.length() > 0)) { + sb.append("NAIF_SURFACE_NAME +=" + "'" + dskParams.get(DSK_KEYS.NAIF_SURFNAME) + "'\n"); + sb.append("NAIF_SURFACE_CODE +=" + dskParams.get(DSK_KEYS.NAIF_SURFCODE) + "\n"); + sb.append("NAIF_SURFACE_BODY +=" + dskParams.get(DSK_KEYS.NAIF_SURFBODY) + "\n"); - System.out.println("currDir:" + currDir.toString()); - System.out.println("mkFile:" + mkFile.toString()); + } else { + System.out.println("optional NAIF body keywords not set. Will not use them in setup file."); + } - // mkFile path relative to currDir - Path relPath = currDir.relativize(mkFile); - String spicefile = relPath.toString(); + sb.append("\\begintext\n"); - if (spicefile.length() > 80) { - System.out.println("Error: pointer to SPICE metakernel kernel file may not be longer than" - + " 80 characters."); - System.out.println("The paths inside the metakernel file can be as long as 255 characters."); - System.exit(1); + try { + FileWriter os = new FileWriter(setupFile); + BufferedWriter out = new BufferedWriter(os); + out.write(sb.toString()); + out.close(); + + } catch (IOException e) { + e.printStackTrace(); + System.err.println("ERROR creating setupfile for OBJ2DSK! Stopping with error!"); + System.exit(1); + } + return setupFile; } + /** + * Evaluate results of string parsing. Throw exception if any resolved to NaN + * + * @param latLonCorners + */ + private static void evaluateCorners(Map latLonCorners) { - // create the content of setup file - StringBuilder sb = new StringBuilder(); - sb.append("\\begindata\n"); - sb.append("COMMENT_FILE = '" + dskParams.get(DSK_KEYS.COMMENTFILE) + "'\n"); - sb.append("LEAPSECONDS_FILE = '" + spicefile + "'\n"); - sb.append("SURFACE_NAME = '" + dskParams.get(DSK_KEYS.SURF_NAME) + "'\n"); - sb.append("CENTER_NAME = '" + dskParams.get(DSK_KEYS.CENTER_NAME) + "'\n"); - sb.append("REF_FRAME_NAME = '" + dskParams.get(DSK_KEYS.REFFRAME_NAME) + "'\n"); - // sb.append("SURFACE_NAME = 'BENNU'\n"); - // sb.append("CENTER_NAME = 'BENNU'\n"); - // sb.append("REF_FRAME_NAME = 'IAU_BENNU'\n"); - sb.append("START_TIME = '1950-JAN-1/00:00:00'\n"); - sb.append("STOP_TIME = '2050-JAN-1/00:00:00'\n"); - sb.append("DATA_CLASS = 2\n"); - sb.append("INPUT_DATA_UNITS = ( 'ANGLES = DEGREES'\n"); - sb.append(" 'DISTANCES = KILOMETERS' )\n"); - sb.append("COORDINATE_SYSTEM = 'LATITUDINAL'\n"); - String valueString = String.format("MINIMUM_LATITUDE = %.5f\n", - latLonCorners.get(HeaderTag.MINLAT.toString())); - sb.append(valueString); - - // out.write("MAXIMUM_LATITUDE = 90\n"); - valueString = String.format("MAXIMUM_LATITUDE = %.5f\n", - latLonCorners.get(HeaderTag.MAXLAT.toString())); - sb.append(valueString); - - // out.write("MINIMUM_LONGITUDE = -180\n"); - valueString = String.format("MINIMUM_LONGITUDE = %.5f\n", - latLonCorners.get(HeaderTag.MINLON.toString())); - sb.append(valueString); - - // out.write("MAXIMUM_LONGITUDE = 180\n"); - valueString = String.format("MAXIMUM_LONGITUDE = %.5f\n", - latLonCorners.get(HeaderTag.MAXLON.toString())); - sb.append(valueString); - - sb.append("DATA_TYPE = 2\n"); - sb.append("PLATE_TYPE = 3\n"); - - String val; - if (fineVoxScale > 0D) { - val = fineVoxScale.toString(); - val = val.trim(); - sb.append("FINE_VOXEL_SCALE = " + val + "\n"); + for (String key : latLonCorners.keySet()) { + if (latLonCorners.get(key) == Double.NaN) { + System.err.println("ERROR! Value for:" + key + " is NaN! Retry wiht proper string double."); + System.err.println("Exiting program!"); + System.exit(1); + } + } } - if (coarseVoxScale > 0) { - val = coarseVoxScale.toString(); - val = val.trim(); - sb.append("COARSE_VOXEL_SCALE = " + val + "\n"); - } - - String naifSurf = dskParams.get(DSK_KEYS.NAIF_SURFNAME); - String naifCode = dskParams.get(DSK_KEYS.NAIF_SURFCODE); - String naifBody = dskParams.get(DSK_KEYS.NAIF_SURFBODY); - if ((naifSurf.length() > 0) && (naifCode.length() > 0) && (naifBody.length() > 0)) { - sb.append("NAIF_SURFACE_NAME +=" + "'" + dskParams.get(DSK_KEYS.NAIF_SURFNAME) + "'\n"); - sb.append("NAIF_SURFACE_CODE +=" + dskParams.get(DSK_KEYS.NAIF_SURFCODE) + "\n"); - sb.append("NAIF_SURFACE_BODY +=" + dskParams.get(DSK_KEYS.NAIF_SURFBODY) + "\n"); - - } else { - System.out.println("optional NAIF body keywords not set. Will not use them in setup file."); - } - - sb.append("\\begintext\n"); - - try { - FileWriter os = new FileWriter(setupFile); - BufferedWriter out = new BufferedWriter(os); - out.write(sb.toString()); - out.close(); - - } catch (IOException e) { - e.printStackTrace(); - System.err.println("ERROR creating setupfile for OBJ2DSK! Stopping with error!"); - System.exit(1); - } - return setupFile; - - } - - /** - * Evaluate results of string parsing. Throw exception if any resolved to NaN - * - * @param latLonCorners - */ - private static void evaluateCorners(Map latLonCorners) { - - for (String key : latLonCorners.keySet()) { - if (latLonCorners.get(key) == Double.NaN) { - System.err.println("ERROR! Value for:" + key + " is NaN! Retry wiht proper string double."); - System.err.println("Exiting program!"); - System.exit(1); - } - } - } - } diff --git a/src/main/java/terrasaur/apps/PointCloudFormatConverter.java b/src/main/java/terrasaur/apps/PointCloudFormatConverter.java index b53246b..d2ca9b3 100644 --- a/src/main/java/terrasaur/apps/PointCloudFormatConverter.java +++ b/src/main/java/terrasaur/apps/PointCloudFormatConverter.java @@ -57,492 +57,470 @@ import vtk.vtkPolyDataWriter; public class PointCloudFormatConverter implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Convert an input point cloud to a new format."; - } + @Override + public String shortDescription() { + return "Convert an input point cloud to a new format."; + } - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = - """ + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = + """ This program converts an input point cloud to a new format. Supported input formats are ASCII, BIN3 (x,y,z), BIN4 (x, y, z, w), BIN7 (t, x, y, z, s/c x, y, z), FITS, ICQ, OBJ, PLY, and VTK. Supported output formats are ASCII, BIN3, OBJ, and VTK. ASCII format is white spaced delimited x y z coordinates. BINARY files must contain double precision x y z coordinates."""; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private FORMATS inFormat; - private FORMATS outFormat; - private vtkPoints pointsXYZ; - // private List receivedIntensity; - private vtkPolyData polyData; - private Vector3 center; - private int halfSize; - private double groundSampleDistance; - private double clip; - private String additionalGMTArgs; - private double mapRadius; - - public static vtkPoints readPointCloud(String filename) { - PointCloudFormatConverter pcfc = new PointCloudFormatConverter(filename, FORMATS.VTK); - pcfc.read(filename, false); - return pcfc.getPoints(); - } - - private PointCloudFormatConverter() {} - - public PointCloudFormatConverter(FORMATS inFormat, String outFilename) { - this(inFormat, FORMATS.formatFromExtension(outFilename)); - } - - public PointCloudFormatConverter(String inFilename, FORMATS outFormat) { - this(FORMATS.formatFromExtension(inFilename), outFormat); - } - - public PointCloudFormatConverter(String inFilename, String outFilename) { - this(FORMATS.formatFromExtension(inFilename), FORMATS.formatFromExtension(outFilename)); - } - - public PointCloudFormatConverter(FORMATS inFormat, FORMATS outFormat) { - this.inFormat = inFormat; - this.outFormat = outFormat; - this.pointsXYZ = new vtkPoints(); - this.polyData = null; - - this.center = null; - this.mapRadius = Math.sqrt(2); - this.halfSize = -1; - this.groundSampleDistance = -1; - this.clip = 1; - this.additionalGMTArgs = ""; - } - - public PointCloudFormatConverter setPoints(vtkPoints pointsXYZ) { - this.pointsXYZ = pointsXYZ; - return this; - } - - public vtkPoints getPoints() { - return pointsXYZ; - } - - public void setClip(Double clip) { - this.clip = clip; - } - - public void setCenter(double[] centerPt) { - center = new Vector3(centerPt); - } - - public void setMapRadius(double mapRadius) { - this.mapRadius = mapRadius; - } - - public PointCloudFormatConverter setHalfSize(int halfSize) { - this.halfSize = halfSize; - return this; - } - - public PointCloudFormatConverter setGroundSampleDistance(double groundSampleDistance) { - this.groundSampleDistance = groundSampleDistance; - return this; - } - - public PointCloudFormatConverter setGMTArgs(String args) { - this.additionalGMTArgs = args; - return this; - } - - public void read(String inFile, boolean inLLR) { - switch (inFormat) { - case ASCII: - try (BufferedReader br = new BufferedReader(new FileReader(inFile))) { - String line = br.readLine(); - while (line != null) { - line = line.trim(); - if (!line.isEmpty() && !line.startsWith("#")) { - String[] parts = line.split("\\s+"); - if (inLLR) { - double lon = Math.toRadians(Double.parseDouble(parts[0].trim())); - double lat = Math.toRadians(Double.parseDouble(parts[1].trim())); - double range = Double.parseDouble(parts[2].trim()); - double[] xyz = new Vector3D(lon, lat).scalarMultiply(range).toArray(); - pointsXYZ.InsertNextPoint(xyz); - } else { - double[] xyz = new double[3]; - xyz[0] = Double.parseDouble(parts[0].trim()); - xyz[1] = Double.parseDouble(parts[1].trim()); - xyz[2] = Double.parseDouble(parts[2].trim()); - pointsXYZ.InsertNextPoint(xyz); - } - } - line = br.readLine(); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - break; - case BIN3: - case BIN4: - case BIN7: - try (DataInputStream dis = - new DataInputStream(new BufferedInputStream(new FileInputStream(inFile)))) { - while (dis.available() > 0) { - if (inFormat == FORMATS.BIN7) { - // skip time field - BinaryUtils.readDoubleAndSwap(dis); - } - if (inLLR) { - double lon = Math.toRadians(BinaryUtils.readDoubleAndSwap(dis)); - double lat = Math.toRadians(BinaryUtils.readDoubleAndSwap(dis)); - double range = BinaryUtils.readDoubleAndSwap(dis); - double[] xyz = new Vector3D(lon, lat).scalarMultiply(range).toArray(); - pointsXYZ.InsertNextPoint(xyz); - } else { - double[] xyz = new double[3]; - xyz[0] = BinaryUtils.readDoubleAndSwap(dis); - xyz[1] = BinaryUtils.readDoubleAndSwap(dis); - xyz[2] = BinaryUtils.readDoubleAndSwap(dis); - pointsXYZ.InsertNextPoint(xyz); - } - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - case ICQ: - case OBJ: - case PLT: - case PLY: - case VTK: - try { - polyData = PolyDataUtil.loadShapeModel(inFile); - pointsXYZ.DeepCopy(polyData.GetPoints()); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - } - break; - case FITS: - try { - polyData = PolyDataUtil.loadFITShapeModel(inFile); - pointsXYZ.DeepCopy(polyData.GetPoints()); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - } - break; - default: - break; + return TerrasaurTool.super.fullDescription(options, header, footer); } - if (clip != 1) { - BoundingBox bbox = new BoundingBox(); - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - double[] point = pointsXYZ.GetPoint(i); - bbox.update(new UnwritableVectorIJK(point[0], point[1], point[2])); - } - BoundingBox clipped = bbox.getScaledBoundingBox(clip); - vtkPoints clippedPoints = new vtkPoints(); - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - if (clipped.contains(pointsXYZ.GetPoint(i))) - clippedPoints.InsertNextPoint(pointsXYZ.GetPoint(i)); - } - pointsXYZ = clippedPoints; - polyData = null; + private FORMATS inFormat; + private FORMATS outFormat; + private vtkPoints pointsXYZ; + // private List receivedIntensity; + private vtkPolyData polyData; + private Vector3 center; + private int halfSize; + private double groundSampleDistance; + private double clip; + private String additionalGMTArgs; + private double mapRadius; + + public static vtkPoints readPointCloud(String filename) { + PointCloudFormatConverter pcfc = new PointCloudFormatConverter(filename, FORMATS.VTK); + pcfc.read(filename, false); + return pcfc.getPoints(); } - } - public void write(String outFile, boolean outLLR) { - switch (outFormat) { - case ASCII: - try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(outFile)))) { - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - double[] thisPoint = pointsXYZ.GetPoint(i); - if (outLLR) { - Vector3D v = new Vector3D(thisPoint); - out.printf( - "%f %f %f\n", - Math.toDegrees(v.getAlpha()), Math.toDegrees(v.getDelta()), v.getNorm()); - } else { - out.printf("%f %f %f\n", thisPoint[0], thisPoint[1], thisPoint[2]); - } - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - break; - case BIN3: - try (DataOutputStream os = - new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile)))) { + private PointCloudFormatConverter() {} - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - double[] thisPoint = pointsXYZ.GetPoint(i); - if (outLLR) { - Vector3D v = new Vector3D(thisPoint); + public PointCloudFormatConverter(FORMATS inFormat, String outFilename) { + this(inFormat, FORMATS.formatFromExtension(outFilename)); + } - BinaryUtils.writeDoubleAndSwap(os, Math.toDegrees(v.getAlpha())); - BinaryUtils.writeDoubleAndSwap(os, Math.toDegrees(v.getDelta())); - BinaryUtils.writeDoubleAndSwap(os, v.getNorm()); - } else { - for (int ii = 0; ii < 3; ii++) BinaryUtils.writeDoubleAndSwap(os, thisPoint[ii]); - } - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); + public PointCloudFormatConverter(String inFilename, FORMATS outFormat) { + this(FORMATS.formatFromExtension(inFilename), outFormat); + } + + public PointCloudFormatConverter(String inFilename, String outFilename) { + this(FORMATS.formatFromExtension(inFilename), FORMATS.formatFromExtension(outFilename)); + } + + public PointCloudFormatConverter(FORMATS inFormat, FORMATS outFormat) { + this.inFormat = inFormat; + this.outFormat = outFormat; + this.pointsXYZ = new vtkPoints(); + this.polyData = null; + + this.center = null; + this.mapRadius = Math.sqrt(2); + this.halfSize = -1; + this.groundSampleDistance = -1; + this.clip = 1; + this.additionalGMTArgs = ""; + } + + public PointCloudFormatConverter setPoints(vtkPoints pointsXYZ) { + this.pointsXYZ = pointsXYZ; + return this; + } + + public vtkPoints getPoints() { + return pointsXYZ; + } + + public void setClip(Double clip) { + this.clip = clip; + } + + public void setCenter(double[] centerPt) { + center = new Vector3(centerPt); + } + + public void setMapRadius(double mapRadius) { + this.mapRadius = mapRadius; + } + + public PointCloudFormatConverter setHalfSize(int halfSize) { + this.halfSize = halfSize; + return this; + } + + public PointCloudFormatConverter setGroundSampleDistance(double groundSampleDistance) { + this.groundSampleDistance = groundSampleDistance; + return this; + } + + public PointCloudFormatConverter setGMTArgs(String args) { + this.additionalGMTArgs = args; + return this; + } + + public void read(String inFile, boolean inLLR) { + switch (inFormat) { + case ASCII: + try (BufferedReader br = new BufferedReader(new FileReader(inFile))) { + String line = br.readLine(); + while (line != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + String[] parts = line.split("\\s+"); + if (inLLR) { + double lon = Math.toRadians(Double.parseDouble(parts[0].trim())); + double lat = Math.toRadians(Double.parseDouble(parts[1].trim())); + double range = Double.parseDouble(parts[2].trim()); + double[] xyz = new Vector3D(lon, lat) + .scalarMultiply(range) + .toArray(); + pointsXYZ.InsertNextPoint(xyz); + } else { + double[] xyz = new double[3]; + xyz[0] = Double.parseDouble(parts[0].trim()); + xyz[1] = Double.parseDouble(parts[1].trim()); + xyz[2] = Double.parseDouble(parts[2].trim()); + pointsXYZ.InsertNextPoint(xyz); + } + } + line = br.readLine(); + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + break; + case BIN3: + case BIN4: + case BIN7: + try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(inFile)))) { + while (dis.available() > 0) { + if (inFormat == FORMATS.BIN7) { + // skip time field + BinaryUtils.readDoubleAndSwap(dis); + } + if (inLLR) { + double lon = Math.toRadians(BinaryUtils.readDoubleAndSwap(dis)); + double lat = Math.toRadians(BinaryUtils.readDoubleAndSwap(dis)); + double range = BinaryUtils.readDoubleAndSwap(dis); + double[] xyz = + new Vector3D(lon, lat).scalarMultiply(range).toArray(); + pointsXYZ.InsertNextPoint(xyz); + } else { + double[] xyz = new double[3]; + xyz[0] = BinaryUtils.readDoubleAndSwap(dis); + xyz[1] = BinaryUtils.readDoubleAndSwap(dis); + xyz[2] = BinaryUtils.readDoubleAndSwap(dis); + pointsXYZ.InsertNextPoint(xyz); + } + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + case ICQ: + case OBJ: + case PLT: + case PLY: + case VTK: + try { + polyData = PolyDataUtil.loadShapeModel(inFile); + pointsXYZ.DeepCopy(polyData.GetPoints()); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + break; + case FITS: + try { + polyData = PolyDataUtil.loadFITShapeModel(inFile); + pointsXYZ.DeepCopy(polyData.GetPoints()); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + break; + default: + break; } - break; - case OBJ: - if (polyData != null) { - try { - PolyDataUtil.saveShapeModelAsOBJ(polyData, outFile); - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - } else { - if (halfSize < 0 || groundSampleDistance < 0) { - logger.error( - "Must supply -halfSize and -groundSampleDistance for {} output", - outFormat); - return; - } - - final double radius = mapRadius * halfSize * groundSampleDistance; - vtkPoints vtkPoints = pointsXYZ; - if (center != null) { - vtkPoints = new vtkPoints(); + if (clip != 1) { + BoundingBox bbox = new BoundingBox(); for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - Vector3 pt = new Vector3(pointsXYZ.GetPoint(i)); - if (center.sub(new Vector3(pt)).norm() > radius) continue; - vtkPoints.InsertNextPoint(pt.toArray()); + double[] point = pointsXYZ.GetPoint(i); + bbox.update(new UnwritableVectorIJK(point[0], point[1], point[2])); } - } - - PointCloudToPlane pctp = new PointCloudToPlane(vtkPoints, halfSize, groundSampleDistance); - pctp.getGMU().setFieldToHeight(); - pctp.getGMU().setGMTArgs(additionalGMTArgs); - try { - double[][][] regridField = pctp.getGMU().regridField(); - vtkPolyData griddedXYZ = PolyDataUtil.loadLocalFitsLLRModelN(regridField); - PolyDataUtil.saveShapeModelAsOBJ(griddedXYZ, outFile); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - } + BoundingBox clipped = bbox.getScaledBoundingBox(clip); + vtkPoints clippedPoints = new vtkPoints(); + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + if (clipped.contains(pointsXYZ.GetPoint(i))) clippedPoints.InsertNextPoint(pointsXYZ.GetPoint(i)); + } + pointsXYZ = clippedPoints; + polyData = null; } - break; - case VTK: - if (polyData == null) { - polyData = new vtkPolyData(); - polyData.SetPoints(pointsXYZ); + } + + public void write(String outFile, boolean outLLR) { + switch (outFormat) { + case ASCII: + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(outFile)))) { + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + double[] thisPoint = pointsXYZ.GetPoint(i); + if (outLLR) { + Vector3D v = new Vector3D(thisPoint); + out.printf( + "%f %f %f\n", + Math.toDegrees(v.getAlpha()), Math.toDegrees(v.getDelta()), v.getNorm()); + } else { + out.printf("%f %f %f\n", thisPoint[0], thisPoint[1], thisPoint[2]); + } + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + break; + case BIN3: + try (DataOutputStream os = + new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile)))) { + + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + double[] thisPoint = pointsXYZ.GetPoint(i); + if (outLLR) { + Vector3D v = new Vector3D(thisPoint); + + BinaryUtils.writeDoubleAndSwap(os, Math.toDegrees(v.getAlpha())); + BinaryUtils.writeDoubleAndSwap(os, Math.toDegrees(v.getDelta())); + BinaryUtils.writeDoubleAndSwap(os, v.getNorm()); + } else { + for (int ii = 0; ii < 3; ii++) BinaryUtils.writeDoubleAndSwap(os, thisPoint[ii]); + } + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + + break; + case OBJ: + if (polyData != null) { + try { + PolyDataUtil.saveShapeModelAsOBJ(polyData, outFile); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } else { + if (halfSize < 0 || groundSampleDistance < 0) { + logger.error("Must supply -halfSize and -groundSampleDistance for {} output", outFormat); + return; + } + + final double radius = mapRadius * halfSize * groundSampleDistance; + vtkPoints vtkPoints = pointsXYZ; + if (center != null) { + vtkPoints = new vtkPoints(); + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + Vector3 pt = new Vector3(pointsXYZ.GetPoint(i)); + if (center.sub(new Vector3(pt)).norm() > radius) continue; + vtkPoints.InsertNextPoint(pt.toArray()); + } + } + + PointCloudToPlane pctp = new PointCloudToPlane(vtkPoints, halfSize, groundSampleDistance); + pctp.getGMU().setFieldToHeight(); + pctp.getGMU().setGMTArgs(additionalGMTArgs); + try { + double[][][] regridField = pctp.getGMU().regridField(); + vtkPolyData griddedXYZ = PolyDataUtil.loadLocalFitsLLRModelN(regridField); + PolyDataUtil.saveShapeModelAsOBJ(griddedXYZ, outFile); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + } + break; + case VTK: + if (polyData == null) { + polyData = new vtkPolyData(); + polyData.SetPoints(pointsXYZ); + } + + vtkCellArray cells = new vtkCellArray(); + vtkFloatArray albedo = new vtkFloatArray(); + albedo.SetName("albedo"); + polyData.SetPolys(cells); + polyData.GetPointData().AddArray(albedo); + + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + vtkIdList idList = new vtkIdList(); + idList.InsertNextId(i); + cells.InsertNextCell(idList); + albedo.InsertNextValue(0.5f); + } + + vtkPolyDataWriter writer = new vtkPolyDataWriter(); + writer.SetInputData(polyData); + writer.SetFileName(outFile); + writer.SetFileTypeToBinary(); + writer.Update(); + break; + default: + break; + } + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("inputFormat") + .hasArg() + .desc("Format of input file. If not present format will be inferred from inputFile extension.") + .build()); + options.addOption(Option.builder("inputFile") + .required() + .hasArg() + .desc("Required. Name of input file.") + .build()); + options.addOption(Option.builder("outputFormat") + .hasArg() + .desc("Format of output file. If not present format will be inferred from outputFile extension.") + .build()); + options.addOption(Option.builder("outputFile") + .required() + .hasArg() + .desc("Required. Name of output file.") + .build()); + options.addOption(Option.builder("inllr") + .desc( + "Only used with ASCII or BINARY formats. If present, input values are assumed to be lon, lat, rad. Default is x, y, z.") + .build()); + options.addOption(Option.builder("outllr") + .desc( + "Only used with ASCII or BINARY formats. If present, output values will be lon, lat, rad. Default is x, y, z.") + .build()); + options.addOption(Option.builder("centerXYZ") + .hasArg() + .desc( + "Only used to generate OBJ output. Center output shape on supplied coordinates. Specify XYZ coordinates as three floating point numbers separated" + + " by commas.") + .build()); + options.addOption(Option.builder("centerLonLat") + .hasArg() + .desc( + "Only used to generate OBJ output. Center output shape on supplied lon,lat. Specify lon,lat in degrees as floating point numbers separated" + + " by a comma. Shape will be centered on the point closest to this lon,lat pair.") + .build()); + options.addOption(Option.builder("halfSize") + .hasArg() + .desc( + "Only used to generate OBJ output. Used with -groundSampleDistance to resample to a uniform grid. Grid dimensions are (2*halfSize+1)x(2*halfSize+1).") + .build()); + options.addOption(Option.builder("groundSampleDistance") + .hasArg() + .desc( + "Used with -halfSize to resample to a uniform grid. Spacing between grid points. Only used to generate OBJ output. " + + "Units are the same as the input file, usually km.") + .build()); + options.addOption(Option.builder("mapRadius") + .hasArg() + .desc( + "Only used to generate OBJ output. Used with -centerXYZ to resample to a uniform grid. Only include points within " + + "mapRadius*groundSampleDistance*halfSize of centerXYZ. Default value is sqrt(2).") + .build()); + options.addOption(Option.builder("gmtArgs") + .hasArg() + .longOpt("gmt-args") + .desc( + "Only used to generate OBJ output. Pass additional options to GMTSurface. May be used multiple times, use once per additional argument.") + .build()); + options.addOption(Option.builder("clip") + .hasArg() + .desc( + "Shrink bounding box to a relative size of and clip any points outside of it. Default is 1 (no clipping).") + .build()); + return options; + } + + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new PointCloudFormatConverter(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + NativeLibraryLoader.loadVtkLibraries(); + NativeLibraryLoader.loadSpiceLibraries(); + + String inFile = cl.getOptionValue("inputFile"); + String outFile = cl.getOptionValue("outputFile"); + boolean inLLR = cl.hasOption("inllr"); + boolean outLLR = cl.hasOption("outllr"); + + FORMATS inFormat = cl.hasOption("inputFormat") + ? FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()) + : FORMATS.formatFromExtension(cl.getOptionValue("inputFile")); + FORMATS outFormat = cl.hasOption("outputFormat") + ? FORMATS.valueOf(cl.getOptionValue("outputFormat").toUpperCase()) + : FORMATS.formatFromExtension(cl.getOptionValue("outputFile")); + + PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inFormat, outFormat); + + if (cl.hasOption("centerXYZ")) { + String[] params = cl.getOptionValue("centerXYZ").split(","); + double[] array = new double[3]; + for (int i = 0; i < 3; i++) array[i] = Double.parseDouble(params[i].trim()); + pcfc.setCenter(array); } - vtkCellArray cells = new vtkCellArray(); - vtkFloatArray albedo = new vtkFloatArray(); - albedo.SetName("albedo"); - polyData.SetPolys(cells); - polyData.GetPointData().AddArray(albedo); - - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - vtkIdList idList = new vtkIdList(); - idList.InsertNextId(i); - cells.InsertNextCell(idList); - albedo.InsertNextValue(0.5f); + if (cl.hasOption("clip")) { + pcfc.setClip(Double.valueOf(cl.getOptionValue("clip"))); } - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputData(polyData); - writer.SetFileName(outFile); - writer.SetFileTypeToBinary(); - writer.Update(); - break; - default: - break; - } - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("inputFormat") - .hasArg() - .desc( - "Format of input file. If not present format will be inferred from inputFile extension.") - .build()); - options.addOption( - Option.builder("inputFile") - .required() - .hasArg() - .desc("Required. Name of input file.") - .build()); - options.addOption( - Option.builder("outputFormat") - .hasArg() - .desc( - "Format of output file. If not present format will be inferred from outputFile extension.") - .build()); - options.addOption( - Option.builder("outputFile") - .required() - .hasArg() - .desc("Required. Name of output file.") - .build()); - options.addOption( - Option.builder("inllr") - .desc( - "Only used with ASCII or BINARY formats. If present, input values are assumed to be lon, lat, rad. Default is x, y, z.") - .build()); - options.addOption( - Option.builder("outllr") - .desc( - "Only used with ASCII or BINARY formats. If present, output values will be lon, lat, rad. Default is x, y, z.") - .build()); - options.addOption( - Option.builder("centerXYZ") - .hasArg() - .desc( - "Only used to generate OBJ output. Center output shape on supplied coordinates. Specify XYZ coordinates as three floating point numbers separated" - + " by commas.") - .build()); - options.addOption( - Option.builder("centerLonLat") - .hasArg() - .desc( - "Only used to generate OBJ output. Center output shape on supplied lon,lat. Specify lon,lat in degrees as floating point numbers separated" - + " by a comma. Shape will be centered on the point closest to this lon,lat pair.") - .build()); - options.addOption( - Option.builder("halfSize") - .hasArg() - .desc( - "Only used to generate OBJ output. Used with -groundSampleDistance to resample to a uniform grid. Grid dimensions are (2*halfSize+1)x(2*halfSize+1).") - .build()); - options.addOption( - Option.builder("groundSampleDistance") - .hasArg() - .desc( - "Used with -halfSize to resample to a uniform grid. Spacing between grid points. Only used to generate OBJ output. " - + "Units are the same as the input file, usually km.") - .build()); - options.addOption( - Option.builder("mapRadius") - .hasArg() - .desc( - "Only used to generate OBJ output. Used with -centerXYZ to resample to a uniform grid. Only include points within " - + "mapRadius*groundSampleDistance*halfSize of centerXYZ. Default value is sqrt(2).") - .build()); - options.addOption( - Option.builder("gmtArgs") - .hasArg() - .longOpt("gmt-args") - .desc( - "Only used to generate OBJ output. Pass additional options to GMTSurface. May be used multiple times, use once per additional argument.") - .build()); - options.addOption( - Option.builder("clip") - .hasArg() - .desc( - "Shrink bounding box to a relative size of and clip any points outside of it. Default is 1 (no clipping).") - .build()); - return options; - } - - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new PointCloudFormatConverter(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadVtkLibraries(); - NativeLibraryLoader.loadSpiceLibraries(); - - String inFile = cl.getOptionValue("inputFile"); - String outFile = cl.getOptionValue("outputFile"); - boolean inLLR = cl.hasOption("inllr"); - boolean outLLR = cl.hasOption("outllr"); - - FORMATS inFormat = - cl.hasOption("inputFormat") - ? FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()) - : FORMATS.formatFromExtension(cl.getOptionValue("inputFile")); - FORMATS outFormat = - cl.hasOption("outputFormat") - ? FORMATS.valueOf(cl.getOptionValue("outputFormat").toUpperCase()) - : FORMATS.formatFromExtension(cl.getOptionValue("outputFile")); - - PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inFormat, outFormat); - - if (cl.hasOption("centerXYZ")) { - String[] params = cl.getOptionValue("centerXYZ").split(","); - double[] array = new double[3]; - for (int i = 0; i < 3; i++) array[i] = Double.parseDouble(params[i].trim()); - pcfc.setCenter(array); - } - - if (cl.hasOption("clip")) { - pcfc.setClip(Double.valueOf(cl.getOptionValue("clip"))); - } - - if (cl.hasOption("gmtArgs")) { - StringBuilder gmtArgs = new StringBuilder(); - for (String arg : cl.getOptionValues("gmtArgs")) gmtArgs.append(String.format("%s ", arg)); - pcfc.setGMTArgs(gmtArgs.toString()); - } - - pcfc.read(inFile, inLLR); - - if (cl.hasOption("centerLonLat")) { - String[] params = cl.getOptionValue("centerLonLat").split(","); - Vector3D lcDir = - new Vector3D( - Math.toRadians(Double.parseDouble(params[0].trim())), - Math.toRadians(Double.parseDouble(params[1].trim()))); - double[] center = null; - double minSep = Double.MAX_VALUE; - vtkPoints vtkPoints = pcfc.getPoints(); - for (int i = 0; i < vtkPoints.GetNumberOfPoints(); i++) { - double[] pt = vtkPoints.GetPoint(i); - double sep = Vector3D.angle(lcDir, new Vector3D(pt)); - if (sep < minSep) { - minSep = sep; - center = pt; + if (cl.hasOption("gmtArgs")) { + StringBuilder gmtArgs = new StringBuilder(); + for (String arg : cl.getOptionValues("gmtArgs")) gmtArgs.append(String.format("%s ", arg)); + pcfc.setGMTArgs(gmtArgs.toString()); } - } - pcfc.setCenter(center); + + pcfc.read(inFile, inLLR); + + if (cl.hasOption("centerLonLat")) { + String[] params = cl.getOptionValue("centerLonLat").split(","); + Vector3D lcDir = new Vector3D( + Math.toRadians(Double.parseDouble(params[0].trim())), + Math.toRadians(Double.parseDouble(params[1].trim()))); + double[] center = null; + double minSep = Double.MAX_VALUE; + vtkPoints vtkPoints = pcfc.getPoints(); + for (int i = 0; i < vtkPoints.GetNumberOfPoints(); i++) { + double[] pt = vtkPoints.GetPoint(i); + double sep = Vector3D.angle(lcDir, new Vector3D(pt)); + if (sep < minSep) { + minSep = sep; + center = pt; + } + } + pcfc.setCenter(center); + } + + pcfc.setMapRadius( + cl.hasOption("mapRadius") ? Double.parseDouble(cl.getOptionValue("mapRadius")) : Math.sqrt(2)); + + if (cl.hasOption("halfSize") && cl.hasOption("groundSampleDistance")) { + // resample on a uniform XY grid + pcfc.setHalfSize(Integer.parseInt(cl.getOptionValue("halfSize"))); + pcfc.setGroundSampleDistance(Double.parseDouble(cl.getOptionValue("groundSampleDistance"))); + } + + pcfc.write(outFile, outLLR); } - - pcfc.setMapRadius( - cl.hasOption("mapRadius") ? Double.parseDouble(cl.getOptionValue("mapRadius")) : Math.sqrt(2)); - - if (cl.hasOption("halfSize") && cl.hasOption("groundSampleDistance")) { - // resample on a uniform XY grid - pcfc.setHalfSize(Integer.parseInt(cl.getOptionValue("halfSize"))); - pcfc.setGroundSampleDistance(Double.parseDouble(cl.getOptionValue("groundSampleDistance"))); - } - - pcfc.write(outFile, outLLR); - } } diff --git a/src/main/java/terrasaur/apps/PointCloudOverlap.java b/src/main/java/terrasaur/apps/PointCloudOverlap.java index 911885c..e5fee29 100644 --- a/src/main/java/terrasaur/apps/PointCloudOverlap.java +++ b/src/main/java/terrasaur/apps/PointCloudOverlap.java @@ -51,367 +51,349 @@ import vtk.vtkPoints; public class PointCloudOverlap implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + 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 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 refPoints) { - List 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())); + @Override + public String shortDescription() { + return "Find points in a point cloud which overlap a reference point cloud."; } - /*- - 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); + @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); + } - for (Vector2D refPoint : refPoints) - ref2DPoints.InsertNextPoint(refPoint.getX(), refPoint.getY(), 0); + 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; */ - MonotoneChain mc = new MonotoneChain(); - List 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); + public PointCloudOverlap(Collection 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 refPoints) { + List 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())); + } - 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); + 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); */ - } - // id0 = id1; - } - polygon.closePath(); - /*- - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputData(polygonPolyData); - writer.SetFileName("polygon2D.vtk"); - writer.SetFileTypeToBinary(); - writer.Update(); + MonotoneChain mc = new MonotoneChain(); + List 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); - writer = new vtkPolyDataWriter(); - polygonPolyData = new vtkPolyData(); - polygonPolyData.SetPoints(ref2DPoints); - writer.SetInputData(polygonPolyData); - writer.SetFileName("refPoints.vtk"); - writer.SetFileTypeToBinary(); - writer.Update(); - */ + 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(); - 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()); - } + writer = new vtkPolyDataWriter(); + polygonPolyData = new vtkPolyData(); + polygonPolyData.SetPoints(ref2DPoints); + writer.SetInputData(polygonPolyData); + writer.SetFileName("refPoints.vtk"); + writer.SetFileTypeToBinary(); + writer.Update(); + */ - /** - * @param inputPoints points to consider - * @param scale scale factor - * @return indices of all points inside the scaled polygon - */ - public List scalePoints(List inputPoints, double scale) { - - List 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 translatedPoints = new ArrayList<>(); - for (Vector2D inputPoint : projected) translatedPoints.add(inputPoint.subtract(center)); - - Path2D.Double thisPolygon = null; - MonotoneChain mc = new MonotoneChain(); - Collection 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 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 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 refPts = new ArrayList<>(); - for (int i = 0; i < referencePoints.GetNumberOfPoints(); i++) { - refPts.add(referencePoints.GetPoint(i)); + 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()); } - // create the overlap object and set the enclosing polygon - PointCloudOverlap pco = new PointCloudOverlap(refPts); + /** + * @param inputPoints points to consider + * @param scale scale factor + * @return indices of all points inside the scaled polygon + */ + public List scalePoints(List inputPoints, double scale) { - // 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"); + List 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())); + } - pcfc = new PointCloudFormatConverter(inFormat, FORMATS.VTK); - pcfc.read(inFile, inLLR); - vtkPoints inputPoints = pcfc.getPoints(); - logger.info("{} points read from {}", inputPoints.GetNumberOfPoints(), inFile); + Vector2D center = new Vector2D(0, 0); + for (Vector2D inputPoint : projected) center = center.add(inputPoint); - List enclosedIndices = new ArrayList<>(); - for (int i = 0; i < inputPoints.GetNumberOfPoints(); i++) { - double[] pt = inputPoints.GetPoint(i); - if (pco.isEnclosed(pt)) enclosedIndices.add(i); + center = center.scalarMultiply(1. / inputPoints.size()); + + List translatedPoints = new ArrayList<>(); + for (Vector2D inputPoint : projected) translatedPoints.add(inputPoint.subtract(center)); + + Path2D.Double thisPolygon = null; + MonotoneChain mc = new MonotoneChain(); + Collection 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 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; } - if (cl.hasOption("scale")) { - List 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 theseIndices = pco.scalePoints(pts, Double.parseDouble(cl.getOptionValue("scale"))); - - // now relate this list back to the original list of points - List newIndices = new ArrayList<>(); - for (Integer i : theseIndices) newIndices.add(enclosedIndices.get(i)); - enclosedIndices = newIndices; + 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; } - 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)); + 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 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 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 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 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 theseIndices = pco.scalePoints(pts, Double.parseDouble(cl.getOptionValue("scale"))); + + // now relate this list back to the original list of points + List 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"); } - - 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 diff --git a/src/main/java/terrasaur/apps/PointCloudToPlane.java b/src/main/java/terrasaur/apps/PointCloudToPlane.java index d326aa3..c9ec23e 100644 --- a/src/main/java/terrasaur/apps/PointCloudToPlane.java +++ b/src/main/java/terrasaur/apps/PointCloudToPlane.java @@ -64,348 +64,320 @@ import vtk.vtkPoints; public class PointCloudToPlane implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Find a rotation and translation to transform a point cloud to a height field above the best fit plane."; - } + @Override + public String shortDescription() { + return "Find a rotation and translation to transform a point cloud to a height field above the best fit plane."; + } - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = - "\nThis program finds a rotation and translation to transform a point cloud to a height field above the best fit plane. " - + "Supported input formats are ASCII, BINARY, L2, OBJ, and VTK.\n\n" - + "ASCII format is white spaced delimited x y z coordinates. BINARY files must contain double precision x y z coordinates. "; - return TerrasaurTool.super.fullDescription(options, header, footer); - } + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = + "\nThis program finds a rotation and translation to transform a point cloud to a height field above the best fit plane. " + + "Supported input formats are ASCII, BINARY, L2, OBJ, and VTK.\n\n" + + "ASCII format is white spaced delimited x y z coordinates. BINARY files must contain double precision x y z coordinates. "; + return TerrasaurTool.super.fullDescription(options, header, footer); + } - private GMTGridUtil gmu; + private GMTGridUtil gmu; - public GMTGridUtil getGMU() { - return gmu; - } + public GMTGridUtil getGMU() { + return gmu; + } - public void writeOutput(String outputFile) { - if (outputFile != null) { - try (PrintStream ps = new PrintStream(outputFile)) { - double[][] transformation = gmu.getTransformation(); - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - ps.printf("%24.16e ", transformation[i][j]); - } - ps.println(); + public void writeOutput(String outputFile) { + if (outputFile != null) { + try (PrintStream ps = new PrintStream(outputFile)) { + double[][] transformation = gmu.getTransformation(); + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + ps.printf("%24.16e ", transformation[i][j]); + } + ps.println(); + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - } - - private PointCloudToPlane() {} - - public PointCloudToPlane(vtkPoints points) { - this(points, 0, 0.); - } - - public PointCloudToPlane(vtkPoints points, int halfSize, double groundSampleDistance) { - double[] x = new double[(int) points.GetNumberOfPoints()]; - double[] y = new double[(int) points.GetNumberOfPoints()]; - double[] z = new double[(int) points.GetNumberOfPoints()]; - - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - double[] thisPoint = points.GetPoint(i); - x[i] = thisPoint[0]; - y[i] = thisPoint[1]; - z[i] = thisPoint[2]; } - gmu = new GMTGridUtil(halfSize, groundSampleDistance); - gmu.setXYZ(x, y, z); - } + private PointCloudToPlane() {} - public BufferedImage makePlot(List points, String name) throws SpiceException { - DescriptiveStatistics stats = new DescriptiveStatistics(); - VectorStatistics vStats = new VectorStatistics(); - DiscreteDataSet data = new DiscreteDataSet(name); - - PlotConfig config = ImmutablePlotConfig.builder().title(name).build(); - - DiscreteDataPlot canvas; - boolean orthographic = false; - if (orthographic) { - for (Vector3 p : points) { - stats.addValue(p.getElt(2)); - vStats.add(p); - LatitudinalCoordinates lc = new LatitudinalCoordinates(p); - data.add(lc.getLongitude(), lc.getLatitude(), 0, p.getElt(2)); - } - - double min = stats.getMin(); - double max = stats.getMax(); - ColorRamp ramp = ColorRamp.create(TYPE.CBSPECTRAL, min, max).createReverse(); - data.setSymbol(new Circle().setSize(1.0)); - data.setColorRamp(ramp); - - Vector3 center = MathConversions.toVector3(vStats.getMean()); - - double halfExtent = 0; - for (Vector3 p : points) { - double dist = center.sep(p); - if (dist > halfExtent) halfExtent = dist; - } - - ProjectionOrthographic p = - new ProjectionOrthographic( - config.width(), - config.height(), - CoordConverters.convertToLatitudinal( - new VectorIJK(center.getElt(0), center.getElt(1), center.getElt(2)))); - p.setRadius(Math.max(0.5, .6 / halfExtent)); - - canvas = new MapPlot(config, p); - canvas.drawAxes(); - canvas.plot(data); - ((MapPlot) canvas).drawLatLonGrid(Math.toRadians(5), Math.toRadians(5), true); - - canvas.drawColorBar( - ImmutableColorBar.builder() - .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) - .ramp(ramp) - .numTicks(5) - .tickFunction(StringFunctions.fixedFormat("%.3f")) - .build()); - } else { - for (Vector3 p : points) { - stats.addValue(p.getElt(2)); - vStats.add(p); - data.add(p.getElt(0), p.getElt(1), 0, p.getElt(2)); - } - - double min = stats.getMin(); - double max = stats.getMax(); - ColorRamp ramp = ColorRamp.create(TYPE.CBSPECTRAL, min, max).createReverse(); - data.setSymbol(new Circle().setSize(1.0)); - data.setColorRamp(ramp); - - canvas = new DiscreteDataPlot(config); - AxisX xAxis = data.defaultXAxis("X"); - AxisY yAxis = data.defaultYAxis("Y"); - canvas.setAxes(xAxis, yAxis); - canvas.drawAxes(); - canvas.plot(data); - - canvas.drawColorBar( - ImmutableColorBar.builder() - .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) - .ramp(ramp) - .numTicks(5) - .tickFunction(StringFunctions.fixedFormat("%.3f")) - .build()); + public PointCloudToPlane(vtkPoints points) { + this(points, 0, 0.); } - return canvas.getImage(); - } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("inputFormat") - .hasArg() - .desc( - "Format of input file. If not present format is inferred from inputFile 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("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("outputFile") - .hasArg() - .desc( - "Name of output file to contain 4x4 transformation matrix. The top left 3x3 matrix is the rotation matrix. The top " - + "three entries in the right hand column are the translation vector. The bottom row is always 0 0 0 1.\nTo convert " - + "from global to local:\n transformed = rotation.mxv(point.sub(translation))") - .build()); - options.addOption( - Option.builder("translate") - .hasArg() - .desc( - "Translate surface points and spacecraft position. " - + "Specify by three floating point numbers separated by commas. " - + "Default is to use centroid of input point cloud.") - .build()); - options.addOption( - Option.builder("plotXYZ") - .hasArg() - .desc( - "Plot X vs Y (in the local frame) colored by Z. " - + "Argument is the name of PNG file to write.") - .build()); - options.addOption( - Option.builder("plotXYR") - .hasArg() - .desc( - "Plot X vs Y (in the local frame) colored by R. " - + "Argument is the name of PNG file to write.") - .build()); - options.addOption( - Option.builder("slope") - .desc( - "Choose local coordinate frame such that Z points normal to the plane " - + "and X points along the direction of steepest descent.") - .build()); - return options; - } + public PointCloudToPlane(vtkPoints points, int halfSize, double groundSampleDistance) { + double[] x = new double[(int) points.GetNumberOfPoints()]; + double[] y = new double[(int) points.GetNumberOfPoints()]; + double[] z = new double[(int) points.GetNumberOfPoints()]; - public static void main(String[] args) throws SpiceException { - TerrasaurTool defaultOBJ = new PointCloudToPlane(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + double[] thisPoint = points.GetPoint(i); + x[i] = thisPoint[0]; + y[i] = thisPoint[1]; + z[i] = thisPoint[2]; + } - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadVtkLibraries(); - NativeLibraryLoader.loadSpiceLibraries(); - - String inFile = cl.getOptionValue("inputFile"); - boolean inLLR = cl.hasOption("inllr"); - - FORMATS inFormat = - cl.hasOption("inputFormat") - ? FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()) - : FORMATS.formatFromExtension(inFile); - - PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inFormat, FORMATS.VTK); - pcfc.read(inFile, inLLR); - System.out.printf("%d points read from %s\n", pcfc.getPoints().GetNumberOfPoints(), inFile); - - int halfSize = 0; - double groundSampleDistance = 0; - - vtkPoints points = pcfc.getPoints(); - PointCloudToPlane pctp = new PointCloudToPlane(points, halfSize, groundSampleDistance); - - Vector3 translation; - if (cl.hasOption("translate")) { - translation = - MathConversions.toVector3(VectorUtils.stringToVector3D(cl.getOptionValue("translate"))); - pctp.getGMU().setTranslation(translation.toArray()); + gmu = new GMTGridUtil(halfSize, groundSampleDistance); + gmu.setXYZ(x, y, z); } - pctp.getGMU().calculateTransformation(); - List globalPts = new ArrayList<>(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) - globalPts.add(new Vector3(points.GetPoint(i))); + public BufferedImage makePlot(List points, String name) throws SpiceException { + DescriptiveStatistics stats = new DescriptiveStatistics(); + VectorStatistics vStats = new VectorStatistics(); + DiscreteDataSet data = new DiscreteDataSet(name); - double[][] transformation = pctp.getGMU().getTransformation(); - StringBuilder sb = - new StringBuilder( - String.format( + PlotConfig config = ImmutablePlotConfig.builder().title(name).build(); + + DiscreteDataPlot canvas; + boolean orthographic = false; + if (orthographic) { + for (Vector3 p : points) { + stats.addValue(p.getElt(2)); + vStats.add(p); + LatitudinalCoordinates lc = new LatitudinalCoordinates(p); + data.add(lc.getLongitude(), lc.getLatitude(), 0, p.getElt(2)); + } + + double min = stats.getMin(); + double max = stats.getMax(); + ColorRamp ramp = ColorRamp.create(TYPE.CBSPECTRAL, min, max).createReverse(); + data.setSymbol(new Circle().setSize(1.0)); + data.setColorRamp(ramp); + + Vector3 center = MathConversions.toVector3(vStats.getMean()); + + double halfExtent = 0; + for (Vector3 p : points) { + double dist = center.sep(p); + if (dist > halfExtent) halfExtent = dist; + } + + ProjectionOrthographic p = new ProjectionOrthographic( + config.width(), + config.height(), + CoordConverters.convertToLatitudinal( + new VectorIJK(center.getElt(0), center.getElt(1), center.getElt(2)))); + p.setRadius(Math.max(0.5, .6 / halfExtent)); + + canvas = new MapPlot(config, p); + canvas.drawAxes(); + canvas.plot(data); + ((MapPlot) canvas).drawLatLonGrid(Math.toRadians(5), Math.toRadians(5), true); + + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.3f")) + .build()); + } else { + for (Vector3 p : points) { + stats.addValue(p.getElt(2)); + vStats.add(p); + data.add(p.getElt(0), p.getElt(1), 0, p.getElt(2)); + } + + double min = stats.getMin(); + double max = stats.getMax(); + ColorRamp ramp = ColorRamp.create(TYPE.CBSPECTRAL, min, max).createReverse(); + data.setSymbol(new Circle().setSize(1.0)); + data.setColorRamp(ramp); + + canvas = new DiscreteDataPlot(config); + AxisX xAxis = data.defaultXAxis("X"); + AxisY yAxis = data.defaultYAxis("Y"); + canvas.setAxes(xAxis, yAxis); + canvas.drawAxes(); + canvas.plot(data); + + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.3f")) + .build()); + } + return canvas.getImage(); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("inputFormat") + .hasArg() + .desc("Format of input file. If not present format is inferred from inputFile 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("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("outputFile") + .hasArg() + .desc( + "Name of output file to contain 4x4 transformation matrix. The top left 3x3 matrix is the rotation matrix. The top " + + "three entries in the right hand column are the translation vector. The bottom row is always 0 0 0 1.\nTo convert " + + "from global to local:\n transformed = rotation.mxv(point.sub(translation))") + .build()); + options.addOption(Option.builder("translate") + .hasArg() + .desc("Translate surface points and spacecraft position. " + + "Specify by three floating point numbers separated by commas. " + + "Default is to use centroid of input point cloud.") + .build()); + options.addOption(Option.builder("plotXYZ") + .hasArg() + .desc("Plot X vs Y (in the local frame) colored by Z. " + "Argument is the name of PNG file to write.") + .build()); + options.addOption(Option.builder("plotXYR") + .hasArg() + .desc("Plot X vs Y (in the local frame) colored by R. " + "Argument is the name of PNG file to write.") + .build()); + options.addOption(Option.builder("slope") + .desc("Choose local coordinate frame such that Z points normal to the plane " + + "and X points along the direction of steepest descent.") + .build()); + return options; + } + + public static void main(String[] args) throws SpiceException { + TerrasaurTool defaultOBJ = new PointCloudToPlane(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + NativeLibraryLoader.loadVtkLibraries(); + NativeLibraryLoader.loadSpiceLibraries(); + + String inFile = cl.getOptionValue("inputFile"); + boolean inLLR = cl.hasOption("inllr"); + + FORMATS inFormat = cl.hasOption("inputFormat") + ? FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()) + : FORMATS.formatFromExtension(inFile); + + PointCloudFormatConverter pcfc = new PointCloudFormatConverter(inFormat, FORMATS.VTK); + pcfc.read(inFile, inLLR); + System.out.printf("%d points read from %s\n", pcfc.getPoints().GetNumberOfPoints(), inFile); + + int halfSize = 0; + double groundSampleDistance = 0; + + vtkPoints points = pcfc.getPoints(); + PointCloudToPlane pctp = new PointCloudToPlane(points, halfSize, groundSampleDistance); + + Vector3 translation; + if (cl.hasOption("translate")) { + translation = MathConversions.toVector3(VectorUtils.stringToVector3D(cl.getOptionValue("translate"))); + pctp.getGMU().setTranslation(translation.toArray()); + } + pctp.getGMU().calculateTransformation(); + + List globalPts = new ArrayList<>(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) globalPts.add(new Vector3(points.GetPoint(i))); + + double[][] transformation = pctp.getGMU().getTransformation(); + StringBuilder sb = new StringBuilder(String.format( "translation vector:\n%24.16e%24.16e%24.16e\n", transformation[0][3], transformation[1][3], transformation[2][3])); - logger.info(sb.toString()); - sb = new StringBuilder("rotation matrix:\n"); - for (int i = 0; i < 3; i++) - sb.append( - String.format( - "%24.16e%24.16e%24.16e\n", - transformation[i][0], transformation[i][1], transformation[i][2])); - logger.info(sb.toString()); + logger.info(sb.toString()); + sb = new StringBuilder("rotation matrix:\n"); + for (int i = 0; i < 3; i++) + sb.append(String.format( + "%24.16e%24.16e%24.16e\n", transformation[i][0], transformation[i][1], transformation[i][2])); + logger.info(sb.toString()); - Matrix33 rotation = new Matrix33(pctp.getGMU().getRotation()); - translation = new Vector3(pctp.getGMU().getTranslation()); + Matrix33 rotation = new Matrix33(pctp.getGMU().getRotation()); + translation = new Vector3(pctp.getGMU().getTranslation()); - if (cl.hasOption("slope")) { - Vector3 z = rotation.xpose().mxv(new Vector3(0, 0, 1)); - VectorStatistics vStats = new VectorStatistics(); - for (Vector3 pt : globalPts) vStats.add(pt); + if (cl.hasOption("slope")) { + Vector3 z = rotation.xpose().mxv(new Vector3(0, 0, 1)); + VectorStatistics vStats = new VectorStatistics(); + for (Vector3 pt : globalPts) vStats.add(pt); - Vector3 r = MathConversions.toVector3(vStats.getMean()); + Vector3 r = MathConversions.toVector3(vStats.getMean()); - Vector3 y = r.cross(z).hat(); - Vector3 x = y.cross(z).hat(); - rotation = new Matrix33(x, y, z); - } - - List localPts = new ArrayList<>(); - for (Vector3 p : globalPts) localPts.add(rotation.mxv(p.sub(translation))); - - VectorStatistics vStats = new VectorStatistics(); - for (Vector3 localPt : localPts) vStats.add(localPt); - - if (cl.hasOption("plotXYZ")) { - BufferedImage image = pctp.makePlot(localPts, "Z (height above plane)"); - PlotCanvas.writeImage(cl.getOptionValue("plotXYZ"), image); - } - - if (cl.hasOption("plotXYR")) { - - // rotate but don't translate - List xyr = new ArrayList<>(); - for (Vector3 p : globalPts) { - Vector3 v = rotation.mxv(p); - xyr.add(new Vector3(v.getElt(0), v.getElt(1), v.norm())); - } - - BufferedImage image = pctp.makePlot(xyr, "R"); - PlotCanvas.writeImage(cl.getOptionValue("plotXYR"), image); - } - - logger.info("statistics on full set"); - logger.info(vStats); - - Vector3 mean = MathConversions.toVector3(vStats.getMean()); - Vector3 std = MathConversions.toVector3(vStats.getStandardDeviation()); - double scale = 5; - List minList = new ArrayList<>(); - List maxList = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - minList.add(mean.getElt(i) - scale * std.getElt(i)); - maxList.add(mean.getElt(i) + scale * std.getElt(i)); - } - - vStats = new VectorStatistics(); - for (Vector3 v : localPts) { - boolean addThis = true; - for (int i = 0; i < 3; i++) { - if (v.getElt(i) < minList.get(i) || v.getElt(i) > maxList.get(i)) { - addThis = false; - break; + Vector3 y = r.cross(z).hat(); + Vector3 x = y.cross(z).hat(); + rotation = new Matrix33(x, y, z); } - } - if (addThis) vStats.add(v); + + List localPts = new ArrayList<>(); + for (Vector3 p : globalPts) localPts.add(rotation.mxv(p.sub(translation))); + + VectorStatistics vStats = new VectorStatistics(); + for (Vector3 localPt : localPts) vStats.add(localPt); + + if (cl.hasOption("plotXYZ")) { + BufferedImage image = pctp.makePlot(localPts, "Z (height above plane)"); + PlotCanvas.writeImage(cl.getOptionValue("plotXYZ"), image); + } + + if (cl.hasOption("plotXYR")) { + + // rotate but don't translate + List xyr = new ArrayList<>(); + for (Vector3 p : globalPts) { + Vector3 v = rotation.mxv(p); + xyr.add(new Vector3(v.getElt(0), v.getElt(1), v.norm())); + } + + BufferedImage image = pctp.makePlot(xyr, "R"); + PlotCanvas.writeImage(cl.getOptionValue("plotXYR"), image); + } + + logger.info("statistics on full set"); + logger.info(vStats); + + Vector3 mean = MathConversions.toVector3(vStats.getMean()); + Vector3 std = MathConversions.toVector3(vStats.getStandardDeviation()); + double scale = 5; + List minList = new ArrayList<>(); + List maxList = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + minList.add(mean.getElt(i) - scale * std.getElt(i)); + maxList.add(mean.getElt(i) + scale * std.getElt(i)); + } + + vStats = new VectorStatistics(); + for (Vector3 v : localPts) { + boolean addThis = true; + for (int i = 0; i < 3; i++) { + if (v.getElt(i) < minList.get(i) || v.getElt(i) > maxList.get(i)) { + addThis = false; + break; + } + } + if (addThis) vStats.add(v); + } + + logger.info("statistics on set without points more than 5 standard deviations from the mean:"); + logger.info(vStats); + + if (cl.hasOption("outputFile")) pctp.writeOutput(cl.getOptionValue("outputFile")); } - - logger.info("statistics on set without points more than 5 standard deviations from the mean:"); - logger.info(vStats); - - if (cl.hasOption("outputFile")) pctp.writeOutput(cl.getOptionValue("outputFile")); - } } diff --git a/src/main/java/terrasaur/apps/PrintShapeModelStatistics.java b/src/main/java/terrasaur/apps/PrintShapeModelStatistics.java index eab9663..aef32d1 100644 --- a/src/main/java/terrasaur/apps/PrintShapeModelStatistics.java +++ b/src/main/java/terrasaur/apps/PrintShapeModelStatistics.java @@ -24,7 +24,6 @@ package terrasaur.apps; import java.util.ArrayList; import java.util.Map; - import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; @@ -45,50 +44,53 @@ import vtk.vtkPolyData; */ public class PrintShapeModelStatistics implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private PrintShapeModelStatistics() {} + private PrintShapeModelStatistics() {} - @Override - public String shortDescription() { - return "Print statistics about a shape model."; - } - - @Override - public String fullDescription(Options options) { - String header = "This program prints various statistics about a shape model in OBJ format."; - String footer = ""; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("objFile").required().hasArg().desc("Path to OBJ file.").build()); - return options; - } - - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new PrintShapeModelStatistics(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - String filename = cl.getOptionValue("objFile"); - - NativeLibraryLoader.loadVtkLibraries(); - - vtkPolyData polydata = PolyDataUtil.loadShapeModelAndComputeNormals(filename); - - PolyDataStatistics stat = new PolyDataStatistics(polydata); - ArrayList stats = stat.getShapeModelStats(); - for (String line : stats) { - logger.info(line); + @Override + public String shortDescription() { + return "Print statistics about a shape model."; + } + + @Override + public String fullDescription(Options options) { + String header = "This program prints various statistics about a shape model in OBJ format."; + String footer = ""; + return TerrasaurTool.super.fullDescription(options, header, footer); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("objFile") + .required() + .hasArg() + .desc("Path to OBJ file.") + .build()); + return options; + } + + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new PrintShapeModelStatistics(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + String filename = cl.getOptionValue("objFile"); + + NativeLibraryLoader.loadVtkLibraries(); + + vtkPolyData polydata = PolyDataUtil.loadShapeModelAndComputeNormals(filename); + + PolyDataStatistics stat = new PolyDataStatistics(polydata); + ArrayList stats = stat.getShapeModelStats(); + for (String line : stats) { + logger.info(line); + } } - } } diff --git a/src/main/java/terrasaur/apps/RangeFromSumFile.java b/src/main/java/terrasaur/apps/RangeFromSumFile.java index 635eda4..f20d334 100644 --- a/src/main/java/terrasaur/apps/RangeFromSumFile.java +++ b/src/main/java/terrasaur/apps/RangeFromSumFile.java @@ -46,384 +46,375 @@ import vtk.vtkIdList; import vtk.vtkPolyData; public class RangeFromSumFile implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Calculate range to surface from a sumfile."; - } + @Override + public String shortDescription() { + return "Calculate range to surface from a sumfile."; + } - @Override - public String fullDescription(Options options) { + @Override + public String fullDescription(Options options) { - String header = ""; - String footer = - """ + String header = ""; + String footer = + """ This program reads a sumfile along with a shape model and \ calculates the range to the surface. NOTE: Spacecraft position is \ assumed to be in kilometers. If not, use the -distanceScale option \ to convert to km. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private SumFile sumFile; - private vtkPolyData polyData; - private SmallBodyModel smallBodyModel; - - private int xOffset; - private int yOffset; - - private long facet; - - private Vector3D scPos; - private Vector3D sunXYZ; - private Vector3D surfaceIntercept; - - private double tiltDeg; - private double tiltDir; - - private double incidence; - private double emission; - private double phase; - - private double scAzimuth; - private double scElevation; - - private double sunAzimuth; - private double sunElevation; - - private DescriptiveStatistics stats; - private double centerX, centerY; - - public DescriptiveStatistics getStats() { - return stats; - } - - private RangeFromSumFile() {} - - public RangeFromSumFile(SumFile sumFile, vtkPolyData polyData) { - - this.sumFile = sumFile; - - int nPixelsX = sumFile.imageWidth(); - int nPixelsY = sumFile.imageHeight(); - centerX = 0.5 * (nPixelsX - 1); - centerY = 0.5 * (nPixelsY - 1); - - this.polyData = polyData; - - smallBodyModel = new SmallBodyModel(polyData); - - scPos = sumFile.scobj().negate(); - sunXYZ = sumFile.sunDirection(); - - stats = new DescriptiveStatistics(); - } - - public void setDistanceScale(double distanceScale) { - this.scPos = sumFile.scobj().scalarMultiply(distanceScale).negate(); - } - - /** - * @param xOffset x offset in pixels - * @param yOffset y offset in pixels - * @return key is cell index, value is surface intercept for the desired pixel offset from the center of the image. - */ - public Map.Entry findIntercept(int xOffset, int yOffset) { - - this.xOffset = xOffset; - this.yOffset = yOffset; - - Vector3D lookDir = new Vector3D(1.0, sumFile.boresight()); - - if (xOffset != 0) { - Vector3D offset = new Vector3D(-xOffset, sumFile.xPerPixel()); - lookDir = lookDir.add(offset); + return TerrasaurTool.super.fullDescription(options, header, footer); } - if (yOffset != 0) { - Vector3D offset = new Vector3D(-yOffset, sumFile.yPerPixel()); - lookDir = lookDir.add(offset); + private SumFile sumFile; + private vtkPolyData polyData; + private SmallBodyModel smallBodyModel; + + private int xOffset; + private int yOffset; + + private long facet; + + private Vector3D scPos; + private Vector3D sunXYZ; + private Vector3D surfaceIntercept; + + private double tiltDeg; + private double tiltDir; + + private double incidence; + private double emission; + private double phase; + + private double scAzimuth; + private double scElevation; + + private double sunAzimuth; + private double sunElevation; + + private DescriptiveStatistics stats; + private double centerX, centerY; + + public DescriptiveStatistics getStats() { + return stats; } - double[] tmp = new double[3]; - facet = smallBodyModel.computeRayIntersection(scPos.toArray(), lookDir.toArray(), tmp); + private RangeFromSumFile() {} - if (facet == -1) { - surfaceIntercept = null; - } else { - surfaceIntercept = new Vector3D(tmp); + public RangeFromSumFile(SumFile sumFile, vtkPolyData polyData) { - vtkIdList idList = new vtkIdList(); - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; + this.sumFile = sumFile; - polyData.GetCellPoints(facet, idList); + int nPixelsX = sumFile.imageWidth(); + int nPixelsY = sumFile.imageHeight(); + centerX = 0.5 * (nPixelsX - 1); + centerY = 0.5 * (nPixelsY - 1); - // get the ids for each point - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); + this.polyData = polyData; - // get points that comprise the cell - polyData.GetPoint(id0, pt0); - polyData.GetPoint(id1, pt1); - polyData.GetPoint(id2, pt2); + smallBodyModel = new SmallBodyModel(polyData); - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + scPos = sumFile.scobj().negate(); + sunXYZ = sumFile.sunDirection(); - Vector3 center3 = MathConversions.toVector3(facet.getCenter()); - Vector3D center3D = MathConversions.toVector3D(facet.getCenter()); - Vector3 normal3 = MathConversions.toVector3(facet.getNormal()); - Vector3D normal3D = MathConversions.toVector3D(facet.getNormal()); - - tiltDeg = Math.toDegrees(center3.sep(normal3)); - if (tiltDeg > 90) tiltDeg = 180 - tiltDeg; - - tiltDir = Tilts.basicTiltDirDeg(surfaceIntercept.getAlpha(), normal3D); - - - incidence = Vector3D.angle(sunXYZ, normal3D); - emission = Vector3D.angle(scPos, normal3D); - phase = Vector3D.angle(sunXYZ, scPos.subtract(center3D)); - - try { - // scPos is in body fixed coordinates - Plane p = new Plane(normal3, center3); - Vector3 projectedNorth = p.project(new Vector3(0, 0, 1).add(center3)).sub(center3); - Vector3 projected = p.project(MathConversions.toVector3(scPos)).sub(center3); - - scAzimuth = projected.sep(projectedNorth); - if (projected.cross(projectedNorth).dot(center3) < 0) scAzimuth = 2 * Math.PI - scAzimuth; - scElevation = Math.PI / 2 - emission; - - // sunXYZ is a unit vector pointing to the sun - projected = p.project(MathConversions.toVector3(sunXYZ).add(center3)).sub(center3); - - sunAzimuth = projected.sep(projectedNorth); - if (projected.cross(projectedNorth).dot(center3) < 0) sunAzimuth = 2 * Math.PI - sunAzimuth; - sunElevation = Math.PI / 2 - incidence; - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - } - - stats.addValue(scPos.distance(surfaceIntercept)); - } - return new AbstractMap.SimpleEntry<>(facet, surfaceIntercept); - } - - public String getHeader(String filename) { - StringBuffer sb = new StringBuffer(); - sb.append("# x increases to the right and y increases down. Top left corner is 0, 0.\n"); - sb.append(String.format("# %s\n", filename)); - sb.append(String.format("%7s", "# x")); - sb.append(String.format("%7s", "y")); - sb.append(StringUtils.center("facet", 8)); - sb.append(StringUtils.center("Tilt", 12)); - sb.append(StringUtils.center("Tilt Dir", 12)); - sb.append(StringUtils.center("s/c position XYZ", 36)); - sb.append(StringUtils.center("surface intercept XYZ", 36)); - sb.append(StringUtils.center("lon", 12)); - sb.append(StringUtils.center("lat", 12)); - sb.append(StringUtils.center("rad", 12)); - sb.append(StringUtils.center("range", 12)); - sb.append(StringUtils.center("inc", 12)); - sb.append(StringUtils.center("ems", 12)); - sb.append(StringUtils.center("phase", 12)); - sb.append(StringUtils.center("s/c az", 12)); - sb.append(StringUtils.center("s/c el", 12)); - sb.append(StringUtils.center("sun az", 12)); - sb.append(StringUtils.center("sun el", 12)); - - sb.append("\n"); - sb.append(String.format("%7s", "# ")); - sb.append(String.format("%7s", "")); - sb.append(String.format("%8s", "")); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(km)", 36)); - sb.append(StringUtils.center("(km)", 36)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(km)", 12)); - sb.append(StringUtils.center("(km)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - sb.append(StringUtils.center("(deg)", 12)); - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%7.2f", xOffset + centerX)); - sb.append(String.format("%7.2f", yOffset + centerY)); - sb.append(String.format("%8d", facet)); - sb.append(String.format("%12.6f", tiltDeg)); - sb.append(String.format("%12.6f", tiltDir)); - sb.append(String.format("%12.6f", scPos.getX())); - sb.append(String.format("%12.6f", scPos.getY())); - sb.append(String.format("%12.6f", scPos.getZ())); - sb.append(String.format("%12.6f", surfaceIntercept.getX())); - sb.append(String.format("%12.6f", surfaceIntercept.getY())); - sb.append(String.format("%12.6f", surfaceIntercept.getZ())); - - double lon = Math.toDegrees(surfaceIntercept.getAlpha()); - if (lon < 0) lon += 360; - sb.append(String.format("%12.6f", lon)); - sb.append(String.format("%12.6f", Math.toDegrees(surfaceIntercept.getDelta()))); - sb.append(String.format("%12.6f", surfaceIntercept.getNorm())); - - sb.append(String.format("%12.6f", scPos.distance(surfaceIntercept))); - sb.append(String.format("%12.6f", Math.toDegrees(incidence))); - sb.append(String.format("%12.6f", Math.toDegrees(emission))); - sb.append(String.format("%12.6f", Math.toDegrees(phase))); - sb.append(String.format("%12.6f", Math.toDegrees(scAzimuth))); - sb.append(String.format("%12.6f", Math.toDegrees(scElevation))); - sb.append(String.format("%12.6f", Math.toDegrees(sunAzimuth))); - sb.append(String.format("%12.6f", Math.toDegrees(sunElevation))); - - return sb.toString(); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("sumFile") - .required() - .hasArg() - .desc("Required. Name of sum file to read.") - .build()); - options.addOption( - Option.builder("objFile") - .required() - .hasArg() - .desc("Required. Name of OBJ shape file.") - .build()); - options.addOption( - Option.builder("pixelOffset") - .hasArg() - .desc( - "Pixel offset from center of image, given as a comma separated pair (no spaces). Default is 0,0. " - + "x increases to the right and y increases down.") - .build()); - options.addOption( - Option.builder("xRange") - .hasArg() - .desc( - "Range of X pixel offsets from center of image, given as a comma separated triplet (xStart, xStop, xSpacing with no spaces). " - + "For example -50,50,5.") - .build()); - options.addOption( - Option.builder("yRange") - .hasArg() - .desc( - "Range of Y pixel offsets from center of image, given as a comma separated triplet (yStart, yStop, ySpacing with no spaces). " - + "For example -50,50,5.") - .build()); - options.addOption( - Option.builder("radius") - .hasArg() - .desc( - "Evaluate all pixels within specified distance (in pixels) of desired pixel. This value will be rounded to the nearest integer.") - .build()); - options.addOption( - Option.builder("distanceScale") - .hasArg() - .desc( - "Spacecraft position is assumed to be in kilometers. If not, scale by this value (e.g. Use 0.001 if s/c pos is in meters).") - .build()); - options.addOption( - Option.builder("stats") - .desc("Print out statistics about range to all selected pixels.") - .build()); - return options; - } - - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new RangeFromSumFile(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - NativeLibraryLoader.loadSpiceLibraries(); - NativeLibraryLoader.loadVtkLibraries(); - - SumFile sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); - - int xStart = 0; - int xStop = 1; - int xSpacing = 1; - int yStart = 0; - int yStop = 1; - int ySpacing = 1; - if (cl.hasOption("pixelOffset")) { - String[] parts = cl.getOptionValue("pixelOffset").split(","); - - int x = Integer.parseInt(parts[0].trim()); - int y = Integer.parseInt(parts[1].trim()); - - xStart = x; - xStop = x + 1; - yStart = y; - yStop = y + 1; + stats = new DescriptiveStatistics(); } - if (cl.hasOption("xRange")) { - String[] parts = cl.getOptionValue("xRange").split(","); - xStart = Integer.parseInt(parts[0].trim()); - xStop = Integer.parseInt(parts[1].trim()); - xSpacing = Integer.parseInt(parts[2].trim()); + public void setDistanceScale(double distanceScale) { + this.scPos = sumFile.scobj().scalarMultiply(distanceScale).negate(); } - if (cl.hasOption("yRange")) { - String[] parts = cl.getOptionValue("yRange").split(","); - yStart = Integer.parseInt(parts[0].trim()); - yStop = Integer.parseInt(parts[1].trim()); - ySpacing = Integer.parseInt(parts[2].trim()); - } + /** + * @param xOffset x offset in pixels + * @param yOffset y offset in pixels + * @return key is cell index, value is surface intercept for the desired pixel offset from the center of the image. + */ + public Map.Entry findIntercept(int xOffset, int yOffset) { - int checkRadius = 0; - if (cl.hasOption("radius")) { - checkRadius = (int) Math.round(Double.parseDouble(cl.getOptionValue("radius"))); - xStart -= checkRadius; - xStop += checkRadius; - yStart -= checkRadius; - yStop += checkRadius; - } + this.xOffset = xOffset; + this.yOffset = yOffset; - String objFile = cl.getOptionValue("objFile"); - vtkPolyData polyData = PolyDataUtil.loadShapeModel(objFile); - RangeFromSumFile rfsf = new RangeFromSumFile(sumFile, polyData); + Vector3D lookDir = new Vector3D(1.0, sumFile.boresight()); - if (cl.hasOption("distanceScale")) - rfsf.setDistanceScale(Double.parseDouble(cl.getOptionValue("distanceScale"))); - - System.out.println(rfsf.getHeader(cl.getOptionValue("sumFile"))); - - for (int ix = xStart; ix < xStop; ix += xSpacing) { - for (int iy = yStart; iy < yStop; iy += ySpacing) { - if (checkRadius > 0) { - double midx = (xStart + xStop) / 2.; - double midy = (yStart + yStop) / 2.; - if ((ix - midx) * (ix - midx) + (iy - midy) * (iy - midy) > checkRadius * checkRadius) - continue; + if (xOffset != 0) { + Vector3D offset = new Vector3D(-xOffset, sumFile.xPerPixel()); + lookDir = lookDir.add(offset); } - long cellID = rfsf.findIntercept(ix, iy).getKey(); - if (cellID > -1) System.out.println(rfsf); - } + + if (yOffset != 0) { + Vector3D offset = new Vector3D(-yOffset, sumFile.yPerPixel()); + lookDir = lookDir.add(offset); + } + + double[] tmp = new double[3]; + facet = smallBodyModel.computeRayIntersection(scPos.toArray(), lookDir.toArray(), tmp); + + if (facet == -1) { + surfaceIntercept = null; + } else { + surfaceIntercept = new Vector3D(tmp); + + vtkIdList idList = new vtkIdList(); + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; + + polyData.GetCellPoints(facet, idList); + + // get the ids for each point + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + + // get points that comprise the cell + polyData.GetPoint(id0, pt0); + polyData.GetPoint(id1, pt1); + polyData.GetPoint(id2, pt2); + + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + + Vector3 center3 = MathConversions.toVector3(facet.getCenter()); + Vector3D center3D = MathConversions.toVector3D(facet.getCenter()); + Vector3 normal3 = MathConversions.toVector3(facet.getNormal()); + Vector3D normal3D = MathConversions.toVector3D(facet.getNormal()); + + tiltDeg = Math.toDegrees(center3.sep(normal3)); + if (tiltDeg > 90) tiltDeg = 180 - tiltDeg; + + tiltDir = Tilts.basicTiltDirDeg(surfaceIntercept.getAlpha(), normal3D); + + incidence = Vector3D.angle(sunXYZ, normal3D); + emission = Vector3D.angle(scPos, normal3D); + phase = Vector3D.angle(sunXYZ, scPos.subtract(center3D)); + + try { + // scPos is in body fixed coordinates + Plane p = new Plane(normal3, center3); + Vector3 projectedNorth = + p.project(new Vector3(0, 0, 1).add(center3)).sub(center3); + Vector3 projected = p.project(MathConversions.toVector3(scPos)).sub(center3); + + scAzimuth = projected.sep(projectedNorth); + if (projected.cross(projectedNorth).dot(center3) < 0) scAzimuth = 2 * Math.PI - scAzimuth; + scElevation = Math.PI / 2 - emission; + + // sunXYZ is a unit vector pointing to the sun + projected = p.project(MathConversions.toVector3(sunXYZ).add(center3)) + .sub(center3); + + sunAzimuth = projected.sep(projectedNorth); + if (projected.cross(projectedNorth).dot(center3) < 0) sunAzimuth = 2 * Math.PI - sunAzimuth; + sunElevation = Math.PI / 2 - incidence; + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + } + + stats.addValue(scPos.distance(surfaceIntercept)); + } + return new AbstractMap.SimpleEntry<>(facet, surfaceIntercept); + } + + public String getHeader(String filename) { + StringBuffer sb = new StringBuffer(); + sb.append("# x increases to the right and y increases down. Top left corner is 0, 0.\n"); + sb.append(String.format("# %s\n", filename)); + sb.append(String.format("%7s", "# x")); + sb.append(String.format("%7s", "y")); + sb.append(StringUtils.center("facet", 8)); + sb.append(StringUtils.center("Tilt", 12)); + sb.append(StringUtils.center("Tilt Dir", 12)); + sb.append(StringUtils.center("s/c position XYZ", 36)); + sb.append(StringUtils.center("surface intercept XYZ", 36)); + sb.append(StringUtils.center("lon", 12)); + sb.append(StringUtils.center("lat", 12)); + sb.append(StringUtils.center("rad", 12)); + sb.append(StringUtils.center("range", 12)); + sb.append(StringUtils.center("inc", 12)); + sb.append(StringUtils.center("ems", 12)); + sb.append(StringUtils.center("phase", 12)); + sb.append(StringUtils.center("s/c az", 12)); + sb.append(StringUtils.center("s/c el", 12)); + sb.append(StringUtils.center("sun az", 12)); + sb.append(StringUtils.center("sun el", 12)); + + sb.append("\n"); + sb.append(String.format("%7s", "# ")); + sb.append(String.format("%7s", "")); + sb.append(String.format("%8s", "")); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(km)", 36)); + sb.append(StringUtils.center("(km)", 36)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(km)", 12)); + sb.append(StringUtils.center("(km)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + sb.append(StringUtils.center("(deg)", 12)); + return sb.toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%7.2f", xOffset + centerX)); + sb.append(String.format("%7.2f", yOffset + centerY)); + sb.append(String.format("%8d", facet)); + sb.append(String.format("%12.6f", tiltDeg)); + sb.append(String.format("%12.6f", tiltDir)); + sb.append(String.format("%12.6f", scPos.getX())); + sb.append(String.format("%12.6f", scPos.getY())); + sb.append(String.format("%12.6f", scPos.getZ())); + sb.append(String.format("%12.6f", surfaceIntercept.getX())); + sb.append(String.format("%12.6f", surfaceIntercept.getY())); + sb.append(String.format("%12.6f", surfaceIntercept.getZ())); + + double lon = Math.toDegrees(surfaceIntercept.getAlpha()); + if (lon < 0) lon += 360; + sb.append(String.format("%12.6f", lon)); + sb.append(String.format("%12.6f", Math.toDegrees(surfaceIntercept.getDelta()))); + sb.append(String.format("%12.6f", surfaceIntercept.getNorm())); + + sb.append(String.format("%12.6f", scPos.distance(surfaceIntercept))); + sb.append(String.format("%12.6f", Math.toDegrees(incidence))); + sb.append(String.format("%12.6f", Math.toDegrees(emission))); + sb.append(String.format("%12.6f", Math.toDegrees(phase))); + sb.append(String.format("%12.6f", Math.toDegrees(scAzimuth))); + sb.append(String.format("%12.6f", Math.toDegrees(scElevation))); + sb.append(String.format("%12.6f", Math.toDegrees(sunAzimuth))); + sb.append(String.format("%12.6f", Math.toDegrees(sunElevation))); + + return sb.toString(); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("sumFile") + .required() + .hasArg() + .desc("Required. Name of sum file to read.") + .build()); + options.addOption(Option.builder("objFile") + .required() + .hasArg() + .desc("Required. Name of OBJ shape file.") + .build()); + options.addOption(Option.builder("pixelOffset") + .hasArg() + .desc( + "Pixel offset from center of image, given as a comma separated pair (no spaces). Default is 0,0. " + + "x increases to the right and y increases down.") + .build()); + options.addOption(Option.builder("xRange") + .hasArg() + .desc( + "Range of X pixel offsets from center of image, given as a comma separated triplet (xStart, xStop, xSpacing with no spaces). " + + "For example -50,50,5.") + .build()); + options.addOption(Option.builder("yRange") + .hasArg() + .desc( + "Range of Y pixel offsets from center of image, given as a comma separated triplet (yStart, yStop, ySpacing with no spaces). " + + "For example -50,50,5.") + .build()); + options.addOption(Option.builder("radius") + .hasArg() + .desc( + "Evaluate all pixels within specified distance (in pixels) of desired pixel. This value will be rounded to the nearest integer.") + .build()); + options.addOption(Option.builder("distanceScale") + .hasArg() + .desc( + "Spacecraft position is assumed to be in kilometers. If not, scale by this value (e.g. Use 0.001 if s/c pos is in meters).") + .build()); + options.addOption(Option.builder("stats") + .desc("Print out statistics about range to all selected pixels.") + .build()); + return options; + } + + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new RangeFromSumFile(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + NativeLibraryLoader.loadSpiceLibraries(); + NativeLibraryLoader.loadVtkLibraries(); + + SumFile sumFile = SumFile.fromFile(new File(cl.getOptionValue("sumFile"))); + + int xStart = 0; + int xStop = 1; + int xSpacing = 1; + int yStart = 0; + int yStop = 1; + int ySpacing = 1; + if (cl.hasOption("pixelOffset")) { + String[] parts = cl.getOptionValue("pixelOffset").split(","); + + int x = Integer.parseInt(parts[0].trim()); + int y = Integer.parseInt(parts[1].trim()); + + xStart = x; + xStop = x + 1; + yStart = y; + yStop = y + 1; + } + + if (cl.hasOption("xRange")) { + String[] parts = cl.getOptionValue("xRange").split(","); + xStart = Integer.parseInt(parts[0].trim()); + xStop = Integer.parseInt(parts[1].trim()); + xSpacing = Integer.parseInt(parts[2].trim()); + } + + if (cl.hasOption("yRange")) { + String[] parts = cl.getOptionValue("yRange").split(","); + yStart = Integer.parseInt(parts[0].trim()); + yStop = Integer.parseInt(parts[1].trim()); + ySpacing = Integer.parseInt(parts[2].trim()); + } + + int checkRadius = 0; + if (cl.hasOption("radius")) { + checkRadius = (int) Math.round(Double.parseDouble(cl.getOptionValue("radius"))); + xStart -= checkRadius; + xStop += checkRadius; + yStart -= checkRadius; + yStop += checkRadius; + } + + String objFile = cl.getOptionValue("objFile"); + vtkPolyData polyData = PolyDataUtil.loadShapeModel(objFile); + RangeFromSumFile rfsf = new RangeFromSumFile(sumFile, polyData); + + if (cl.hasOption("distanceScale")) + rfsf.setDistanceScale(Double.parseDouble(cl.getOptionValue("distanceScale"))); + + System.out.println(rfsf.getHeader(cl.getOptionValue("sumFile"))); + + for (int ix = xStart; ix < xStop; ix += xSpacing) { + for (int iy = yStart; iy < yStop; iy += ySpacing) { + if (checkRadius > 0) { + double midx = (xStart + xStop) / 2.; + double midy = (yStart + yStop) / 2.; + if ((ix - midx) * (ix - midx) + (iy - midy) * (iy - midy) > checkRadius * checkRadius) continue; + } + long cellID = rfsf.findIntercept(ix, iy).getKey(); + if (cellID > -1) System.out.println(rfsf); + } + } + if (cl.hasOption("stats")) System.out.println("Range " + rfsf.getStats()); } - if (cl.hasOption("stats")) System.out.println("Range " + rfsf.getStats()); - } } diff --git a/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java b/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java index 529cb3e..d02c290 100644 --- a/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java +++ b/src/main/java/terrasaur/apps/RenderShapeFromSumFile.java @@ -69,601 +69,595 @@ import vtk.vtkPolyData; public class RenderShapeFromSumFile implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Render a simulated camera image given a shape model and sumFile."; - } - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = "\nRender a simulated camera image given a shape model and sumFile.\n"; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private RenderShapeFromSumFile() {} - - private String globalOBJname; - private Double scale; - private Rotation rotation; - - /** Sun position in body fixed coordinates */ - private Vector3D sunXYZ; - - /** Set camera position in body fixed coordinates */ - private Vector3D cameraXYZ; - - private Rotation cameraToBodyFixed; - - /** set instantaneous field of view in radians/pixel */ - private double ifov; - - private int nPixelsX, nPixelsY; - private double centerX, centerY; - private int subPixel; - - private ThreadLocal sbm; - // key is cell index, value is albedo - private Map albedoMap; - // key is resolution, value is local shape model - private NavigableMap lmcMap; - - // key is field name, value is pair of comment and metadata value - private NavigableMap> metadata; - - public RenderShapeFromSumFile(String globalOBJname, Double scale, Rotation rotation) { - this.globalOBJname = globalOBJname; - this.scale = scale; - this.rotation = rotation; - - subPixel = 2; - - sbm = new ThreadLocal<>(); - albedoMap = new HashMap<>(); - lmcMap = new TreeMap<>(); - - metadata = new TreeMap<>(); - } - - private void loadAlbedoFile(String albedoFile) { - try { - List lines = FileUtils.readLines(new File(albedoFile), Charset.defaultCharset()); - for (String line : lines) { - String trimLine = line.strip(); - if (trimLine.isEmpty() || trimLine.startsWith("#")) continue; - - String[] parts = trimLine.split(","); - long index = Long.parseLong(parts[0].trim()); - albedoMap.put(index, Double.parseDouble(parts[1].trim())); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - private SmallBodyModel getGlobalModel() { - if (sbm.get() == null) { - try { - vtkPolyData model = PolyDataUtil.loadShapeModel(globalOBJname); - if (scale != null || rotation != null) { - PolyDataStatistics stats = new PolyDataStatistics(model); - Vector3D center = new Vector3D(stats.getCentroid()); - - vtkPoints points = model.GetPoints(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - Vector3D thisPoint = new Vector3D(points.GetPoint(i)); - if (scale != null) - thisPoint = thisPoint.subtract(center).scalarMultiply(scale).add(center); - if (rotation != null) - thisPoint = rotation.applyTo(thisPoint.subtract(center)).add(center); - points.SetPoint(i, thisPoint.toArray()); - } - } - sbm.set(new SmallBodyModel(model)); - } catch (Exception e) { - logger.error(e.getLocalizedMessage()); - } - } - return sbm.get(); - } - - public void addMetaData(String key, String comment, String value) { - metadata.put(key, new AbstractMap.SimpleEntry<>(comment, value)); - } - - public void setSubPixel(int subPixel) { - this.subPixel = subPixel; - } - - /** - * Return a unit 3D vector in the body fixed frame given pixel coordinates x and y. (0,0) is the - * upper left corner with X increasing to the right and Y increasing down. - * - * @param ix pixel x value - * @param iy pixel y value - * @return look direction - */ - public Vector3D pixelToBodyFixed(double ix, double iy) { - - double[] xyz = new double[3]; - xyz[0] = FastMath.sin(ifov * (ix - centerX)); - xyz[1] = FastMath.sin(ifov * (iy - centerY)); - xyz[2] = FastMath.sqrt(1 - xyz[0] * xyz[0] - xyz[1] * xyz[1]); - - return cameraToBodyFixed.applyTo(new Vector3D(xyz)); - } - - private static class Brightness { - private final double incidence; - private final double emission; - private final double phase; - private final double brightness; - private final double range; - private final double facetX; - private final double facetY; - private final double facetZ; - private final double normalX; - private final double normalY; - private final double normalZ; - - private Brightness( - double incidence, - double emission, - double phase, - double brightness, - double range, - Vector3D facet, - Vector3D normal) { - this.incidence = Math.toDegrees(incidence); - this.emission = Math.toDegrees(emission); - this.phase = Math.toDegrees(phase); - this.brightness = brightness; - this.range = range; - this.facetX = facet.getX(); - this.facetY = facet.getY(); - this.facetZ = facet.getZ(); - this.normalX = normal.getX(); - this.normalY = normal.getY(); - this.normalZ = normal.getZ(); - } - - private double[] values() { - return new double[] { - this.brightness, - this.incidence, - this.emission, - this.phase, - this.range, - this.facetX, - this.facetY, - this.facetZ, - this.normalX, - this.normalY, - this.normalZ - }; - } - } - - /** - * @param pf Photometric function - * @param intersect cell id of intersection point - * @param intersectPoint XYZ coordinates of intersection point - * @param isDefault true if this is the default model, false if local - * @return Brightness structure - */ - private Brightness getBrightness( - PhotometricFunction pf, - SmallBodyModel sbm, - long intersect, - Vector3D intersectPoint, - boolean isDefault) { - - Vector3D facetToCamera = cameraXYZ.subtract(intersectPoint); - - CellInfo ci = CellInfo.getCellInfo(sbm.getSmallBodyPolyData(), intersect, new vtkIdList()); - Vector3D normal = ci.normal(); - - double emission = Vector3D.angle(facetToCamera, normal); - double distFromCamera = facetToCamera.getNorm(); - - // speeds up calculation along the limb. Need to combine all pixels in the ifov. - double kmPerPixel = - ifov * distFromCamera / Math.abs(FastMath.cos(Math.min(Math.toRadians(60), emission))); - - double sum = 0; - - Set cells = sbm.findClosestCellsWithinRadius(intersectPoint.toArray(), kmPerPixel / 2); - cells.add(intersect); - double incidence = 0; - double phase = 0; - for (long cell : cells) { - - ci = CellInfo.getCellInfo(sbm.getSmallBodyPolyData(), cell, new vtkIdList(), true); - facetToCamera = cameraXYZ.subtract(ci.center()); - normal = new Vector3D(sbm.getCellNormals().GetTuple3(cell)); - emission = Vector3D.angle(facetToCamera, normal); - incidence = 0; - phase = 0; - - if (sunXYZ != null) { - incidence = Vector3D.angle(sunXYZ, normal); - phase = Vector3D.angle(facetToCamera, sunXYZ); - - Vector3D sunToFacet = ci.center().subtract(sunXYZ); - - // check for shadowing - double[] sunIntersectPoint = new double[3]; - long sunIntersect = - sbm.computeRayIntersection(sunXYZ.toArray(), sunToFacet.toArray(), sunIntersectPoint); - if (sunIntersect != cell) { - // don't allow points in shadow to have a 0 value - sum += .001; - continue; - } - } - double albedo = (isDefault && albedoMap.containsKey(cell)) ? albedoMap.get(cell) : 1; - sum += - albedo - * pf.getValue( - FastMath.cos(incidence), FastMath.cos(emission), FastMath.toDegrees(phase)); - } - - logger.printf( - Level.DEBUG, - "Thread %d lat/lon %.2f/%.2f, %s, sum %f, cells %d, %.2f", - Thread.currentThread().threadId(), - Math.toDegrees(intersectPoint.getDelta()), - Math.toDegrees(intersectPoint.getAlpha()), - intersectPoint.toString(), - sum, - cells.size(), - sum / cells.size()); - - return new Brightness( - incidence, emission, phase, sum / cells.size(), distFromCamera, facetToCamera, normal); - } - - class BrightnessCalculator implements Callable> { - - Collection pixelIndices; - PhotometricFunction pf; - - private BrightnessCalculator(Collection pixelIndices, PhotometricFunction pf) { - this.pixelIndices = pixelIndices; - this.pf = pf; + @Override + public String shortDescription() { + return "Render a simulated camera image given a shape model and sumFile."; } @Override - public Map call() throws Exception { + public String fullDescription(Options options) { + String header = ""; + String footer = "\nRender a simulated camera image given a shape model and sumFile.\n"; + return TerrasaurTool.super.fullDescription(options, header, footer); + } - logger.info("Thread {}: starting", Thread.currentThread().threadId()); + private RenderShapeFromSumFile() {} - int xPixels = subPixel * nPixelsX; + private String globalOBJname; + private Double scale; + private Rotation rotation; - SmallBodyModel globalModel = getGlobalModel(); + /** Sun position in body fixed coordinates */ + private Vector3D sunXYZ; - Map brightness = new HashMap<>(); - double[] intersectPoint = new double[3]; - double[] cameraXYZArray = cameraXYZ.toArray(); - for (Integer index : pixelIndices) { - int j = index / xPixels; - int i = index % xPixels; - Vector3D pixelDir = pixelToBodyFixed(((double) i) / subPixel, ((double) j) / subPixel); + /** Set camera position in body fixed coordinates */ + private Vector3D cameraXYZ; - long intersect = - globalModel.computeRayIntersection(cameraXYZArray, pixelDir.toArray(), intersectPoint); + private Rotation cameraToBodyFixed; - if (intersect > -1) { + /** set instantaneous field of view in radians/pixel */ + private double ifov; - Vector3D intersectPt3D = new Vector3D(intersectPoint); + private int nPixelsX, nPixelsY; + private double centerX, centerY; + private int subPixel; - // resolution in m/pixel - double resolution = ifov * intersectPt3D.distance(cameraXYZ) * 1e3; + private ThreadLocal sbm; + // key is cell index, value is albedo + private Map albedoMap; + // key is resolution, value is local shape model + private NavigableMap lmcMap; - // if no ceiling entry exists, stick with the global model - Entry lmcEntry = lmcMap.ceilingEntry(resolution); - if (lmcEntry != null) { + // key is field name, value is pair of comment and metadata value + private NavigableMap> metadata; - LocalModelCollection lmc = lmcEntry.getValue(); - double[] localIntersectPoint = new double[3]; + public RenderShapeFromSumFile(String globalOBJname, Double scale, Rotation rotation) { + this.globalOBJname = globalOBJname; + this.scale = scale; + this.rotation = rotation; - SmallBodyModel localModel = lmc.get(intersectPt3D); - if (localModel != null) { - long localIntersect = - localModel.computeRayIntersection( - cameraXYZArray, pixelDir.toArray(), localIntersectPoint); - if (localIntersect != -1) { - break; - } else { - 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().threadId(), - i, - j, - Math.toDegrees(intersectPt3D.getDelta()), - Math.toDegrees(intersectPt3D.getAlpha()), - intersect, - intersectPt3D)); - } + subPixel = 2; + + sbm = new ThreadLocal<>(); + albedoMap = new HashMap<>(); + lmcMap = new TreeMap<>(); + + metadata = new TreeMap<>(); + } + + private void loadAlbedoFile(String albedoFile) { + try { + List lines = FileUtils.readLines(new File(albedoFile), Charset.defaultCharset()); + for (String line : lines) { + String trimLine = line.strip(); + if (trimLine.isEmpty() || trimLine.startsWith("#")) continue; + + String[] parts = trimLine.split(","); + long index = Long.parseLong(parts[0].trim()); + albedoMap.put(index, Double.parseDouble(parts[1].trim())); + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + private SmallBodyModel getGlobalModel() { + if (sbm.get() == null) { + try { + vtkPolyData model = PolyDataUtil.loadShapeModel(globalOBJname); + if (scale != null || rotation != null) { + PolyDataStatistics stats = new PolyDataStatistics(model); + Vector3D center = new Vector3D(stats.getCentroid()); + + vtkPoints points = model.GetPoints(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + Vector3D thisPoint = new Vector3D(points.GetPoint(i)); + if (scale != null) + thisPoint = thisPoint + .subtract(center) + .scalarMultiply(scale) + .add(center); + if (rotation != null) + thisPoint = + rotation.applyTo(thisPoint.subtract(center)).add(center); + points.SetPoint(i, thisPoint.toArray()); + } + } + sbm.set(new SmallBodyModel(model)); + } catch (Exception e) { + logger.error(e.getLocalizedMessage()); + } + } + return sbm.get(); + } + + public void addMetaData(String key, String comment, String value) { + metadata.put(key, new AbstractMap.SimpleEntry<>(comment, value)); + } + + public void setSubPixel(int subPixel) { + this.subPixel = subPixel; + } + + /** + * Return a unit 3D vector in the body fixed frame given pixel coordinates x and y. (0,0) is the + * upper left corner with X increasing to the right and Y increasing down. + * + * @param ix pixel x value + * @param iy pixel y value + * @return look direction + */ + public Vector3D pixelToBodyFixed(double ix, double iy) { + + double[] xyz = new double[3]; + xyz[0] = FastMath.sin(ifov * (ix - centerX)); + xyz[1] = FastMath.sin(ifov * (iy - centerY)); + xyz[2] = FastMath.sqrt(1 - xyz[0] * xyz[0] - xyz[1] * xyz[1]); + + return cameraToBodyFixed.applyTo(new Vector3D(xyz)); + } + + private static class Brightness { + private final double incidence; + private final double emission; + private final double phase; + private final double brightness; + private final double range; + private final double facetX; + private final double facetY; + private final double facetZ; + private final double normalX; + private final double normalY; + private final double normalZ; + + private Brightness( + double incidence, + double emission, + double phase, + double brightness, + double range, + Vector3D facet, + Vector3D normal) { + this.incidence = Math.toDegrees(incidence); + this.emission = Math.toDegrees(emission); + this.phase = Math.toDegrees(phase); + this.brightness = brightness; + this.range = range; + this.facetX = facet.getX(); + this.facetY = facet.getY(); + this.facetZ = facet.getZ(); + this.normalX = normal.getX(); + this.normalY = normal.getY(); + this.normalZ = normal.getZ(); + } + + private double[] values() { + return new double[] { + this.brightness, + this.incidence, + this.emission, + this.phase, + this.range, + this.facetX, + this.facetY, + this.facetZ, + this.normalX, + this.normalY, + this.normalZ + }; + } + } + + /** + * @param pf Photometric function + * @param intersect cell id of intersection point + * @param intersectPoint XYZ coordinates of intersection point + * @param isDefault true if this is the default model, false if local + * @return Brightness structure + */ + private Brightness getBrightness( + PhotometricFunction pf, SmallBodyModel sbm, long intersect, Vector3D intersectPoint, boolean isDefault) { + + Vector3D facetToCamera = cameraXYZ.subtract(intersectPoint); + + CellInfo ci = CellInfo.getCellInfo(sbm.getSmallBodyPolyData(), intersect, new vtkIdList()); + Vector3D normal = ci.normal(); + + double emission = Vector3D.angle(facetToCamera, normal); + double distFromCamera = facetToCamera.getNorm(); + + // speeds up calculation along the limb. Need to combine all pixels in the ifov. + double kmPerPixel = ifov * distFromCamera / Math.abs(FastMath.cos(Math.min(Math.toRadians(60), emission))); + + double sum = 0; + + Set cells = sbm.findClosestCellsWithinRadius(intersectPoint.toArray(), kmPerPixel / 2); + cells.add(intersect); + double incidence = 0; + double phase = 0; + for (long cell : cells) { + + ci = CellInfo.getCellInfo(sbm.getSmallBodyPolyData(), cell, new vtkIdList(), true); + facetToCamera = cameraXYZ.subtract(ci.center()); + normal = new Vector3D(sbm.getCellNormals().GetTuple3(cell)); + emission = Vector3D.angle(facetToCamera, normal); + incidence = 0; + phase = 0; + + if (sunXYZ != null) { + incidence = Vector3D.angle(sunXYZ, normal); + phase = Vector3D.angle(facetToCamera, sunXYZ); + + Vector3D sunToFacet = ci.center().subtract(sunXYZ); + + // check for shadowing + double[] sunIntersectPoint = new double[3]; + long sunIntersect = + sbm.computeRayIntersection(sunXYZ.toArray(), sunToFacet.toArray(), sunIntersectPoint); + if (sunIntersect != cell) { + // don't allow points in shadow to have a 0 value + sum += .001; + continue; + } + } + double albedo = (isDefault && albedoMap.containsKey(cell)) ? albedoMap.get(cell) : 1; + sum += albedo * pf.getValue(FastMath.cos(incidence), FastMath.cos(emission), FastMath.toDegrees(phase)); + } + + logger.printf( + Level.DEBUG, + "Thread %d lat/lon %.2f/%.2f, %s, sum %f, cells %d, %.2f", + Thread.currentThread().threadId(), + Math.toDegrees(intersectPoint.getDelta()), + Math.toDegrees(intersectPoint.getAlpha()), + intersectPoint.toString(), + sum, + cells.size(), + sum / cells.size()); + + return new Brightness(incidence, emission, phase, sum / cells.size(), distFromCamera, facetToCamera, normal); + } + + class BrightnessCalculator implements Callable> { + + Collection pixelIndices; + PhotometricFunction pf; + + private BrightnessCalculator(Collection pixelIndices, PhotometricFunction pf) { + this.pixelIndices = pixelIndices; + this.pf = pf; + } + + @Override + public Map call() throws Exception { + + logger.info("Thread {}: starting", Thread.currentThread().threadId()); + + int xPixels = subPixel * nPixelsX; + + SmallBodyModel globalModel = getGlobalModel(); + + Map brightness = new HashMap<>(); + double[] intersectPoint = new double[3]; + double[] cameraXYZArray = cameraXYZ.toArray(); + for (Integer index : pixelIndices) { + int j = index / xPixels; + int i = index % xPixels; + Vector3D pixelDir = pixelToBodyFixed(((double) i) / subPixel, ((double) j) / subPixel); + + long intersect = globalModel.computeRayIntersection(cameraXYZArray, pixelDir.toArray(), intersectPoint); + + if (intersect > -1) { + + Vector3D intersectPt3D = new Vector3D(intersectPoint); + + // resolution in m/pixel + double resolution = ifov * intersectPt3D.distance(cameraXYZ) * 1e3; + + // if no ceiling entry exists, stick with the global model + Entry lmcEntry = lmcMap.ceilingEntry(resolution); + if (lmcEntry != null) { + + LocalModelCollection lmc = lmcEntry.getValue(); + double[] localIntersectPoint = new double[3]; + + SmallBodyModel localModel = lmc.get(intersectPt3D); + if (localModel != null) { + long localIntersect = localModel.computeRayIntersection( + cameraXYZArray, pixelDir.toArray(), localIntersectPoint); + if (localIntersect != -1) { + break; + } else { + 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().threadId(), + i, + j, + Math.toDegrees(intersectPt3D.getDelta()), + Math.toDegrees(intersectPt3D.getAlpha()), + intersect, + intersectPt3D)); + } + } + } + + boolean isDefault = lmcEntry == null; + Brightness b = getBrightness(pf, globalModel, intersect, intersectPt3D, isDefault); + brightness.put(j * xPixels + i, b); + } + } + + logger.info("Thread {}: finished", Thread.currentThread().threadId()); + + return brightness; + } + } + + public double[][][] getFits(PhotometricFunction pf, int numThreads) { + + int xPixels = subPixel * nPixelsX; + int yPixels = subPixel * nPixelsY; + + Map brightness = new HashMap<>(); + try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) { + + List indices = + IntStream.range(0, yPixels * xPixels).boxed().toList(); + + int numPixels = indices.size(); + + Set callables = new HashSet<>(); + for (int i = 0; i < numThreads; i++) { + int fromIndex = i * numPixels / numThreads; + int toIndex = Math.min(numPixels, fromIndex + numPixels / numThreads); + callables.add(new BrightnessCalculator(indices.subList(fromIndex, toIndex), pf)); + } + + Set>> futures = new HashSet<>(); + for (BrightnessCalculator callable : callables) futures.add(executor.submit(callable)); + + for (Future> future : futures) { + try { + Map values = future.get(); + brightness.putAll(values); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + } + executor.shutdown(); + } + + double[][][] img = new double[11][yPixels][xPixels]; + for (int i = 0; i < xPixels; i++) { + for (int j = 0; j < yPixels; j++) { + Brightness pixel = brightness.get(j * xPixels + i); + if (pixel == null) continue; + double[] pixels = pixel.values(); + for (int k = 0; k < img.length; k++) { + img[k][yPixels - j - 1][i] = pixels[k]; + } + } + } + return img; + } + + public BufferedImage getImage(PhotometricFunction pf, int numThreads) { + + int xPixels = subPixel * nPixelsX; + int yPixels = subPixel * nPixelsY; + BufferedImage image = new BufferedImage(xPixels, yPixels, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setComposite(AlphaComposite.Src); + g.setColor(Color.BLACK); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + Map brightness = new HashMap<>(); + double maxBrightness = -Double.MAX_VALUE; + + try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) { + + List indices = + IntStream.range(0, yPixels * xPixels).boxed().toList(); + + int numPixels = indices.size(); + + Set callables = new HashSet<>(); + for (int i = 0; i < numThreads; i++) { + int fromIndex = i * numPixels / numThreads; + int toIndex = Math.min(numPixels, fromIndex + numPixels / numThreads); + callables.add(new BrightnessCalculator(indices.subList(fromIndex, toIndex), pf)); + } + + Set>> futures = new HashSet<>(); + for (BrightnessCalculator callable : callables) futures.add(executor.submit(callable)); + + for (Future> future : futures) { + try { + Map brightnessMap = future.get(); + for (Brightness b : brightnessMap.values()) + if (maxBrightness < b.brightness) maxBrightness = b.brightness; + brightness.putAll(future.get()); + } catch (Exception e) { + logger.error(e.getLocalizedMessage(), e); + } + } + executor.shutdown(); + } + + /*- + double[] intersectPoint = new double[3]; + + Map brightness = new HashMap<>(); + + double[] cameraXYZArray = cameraXYZ.toArray(); + for (int i = 0; i < xPixels; i++) { + for (int j = 0; j < yPixels; j++) { + Vector3D pixelDir = pixelToBodyFixed(((double) i) / scale, ((double) j) / scale); + + int intersect = + sbm.computeRayIntersection(cameraXYZArray, pixelDir.toArray(), intersectPoint); + if (intersect > 0) { + brightness.put(i * xPixels + j, + getBrightness(pf, intersect, new Vector3D(intersectPoint))); } } - - boolean isDefault = lmcEntry == null; - Brightness b = getBrightness(pf, globalModel, intersect, intersectPt3D, isDefault); - brightness.put(j * xPixels + i, b); } - } - - logger.info("Thread {}: finished", Thread.currentThread().threadId()); - - return brightness; - } - } - - public double[][][] getFits(PhotometricFunction pf, int numThreads) { - - int xPixels = subPixel * nPixelsX; - int yPixels = subPixel * nPixelsY; - - Map brightness = new HashMap<>(); - try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) { - - List indices = IntStream.range(0, yPixels * xPixels).boxed().toList(); - - int numPixels = indices.size(); - - Set callables = new HashSet<>(); - for (int i = 0; i < numThreads; i++) { - int fromIndex = i * numPixels / numThreads; - int toIndex = Math.min(numPixels, fromIndex + numPixels / numThreads); - callables.add(new BrightnessCalculator(indices.subList(fromIndex, toIndex), pf)); - } - - Set>> futures = new HashSet<>(); - for (BrightnessCalculator callable : callables) futures.add(executor.submit(callable)); - - for (Future> future : futures) { - try { - Map values = future.get(); - brightness.putAll(values); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - } - } - executor.shutdown(); - } - - double[][][] img = new double[11][yPixels][xPixels]; - for (int i = 0; i < xPixels; i++) { - for (int j = 0; j < yPixels; j++) { - Brightness pixel = brightness.get(j * xPixels + i); - if (pixel == null) continue; - double[] pixels = pixel.values(); - for (int k = 0; k < img.length; k++) { - img[k][yPixels - j - 1][i] = pixels[k]; - } - } - } - return img; - } - - public BufferedImage getImage(PhotometricFunction pf, int numThreads) { - - int xPixels = subPixel * nPixelsX; - int yPixels = subPixel * nPixelsY; - BufferedImage image = new BufferedImage(xPixels, yPixels, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = image.createGraphics(); - g.setComposite(AlphaComposite.Clear); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - g.setComposite(AlphaComposite.Src); - g.setColor(Color.BLACK); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - - Map brightness = new HashMap<>(); - double maxBrightness = -Double.MAX_VALUE; - - try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) { - - List indices = IntStream.range(0, yPixels * xPixels).boxed().toList(); - - int numPixels = indices.size(); - - Set callables = new HashSet<>(); - for (int i = 0; i < numThreads; i++) { - int fromIndex = i * numPixels / numThreads; - int toIndex = Math.min(numPixels, fromIndex + numPixels / numThreads); - callables.add(new BrightnessCalculator(indices.subList(fromIndex, toIndex), pf)); - } - - Set>> futures = new HashSet<>(); - for (BrightnessCalculator callable : callables) futures.add(executor.submit(callable)); - - for (Future> future : futures) { - try { - Map brightnessMap = future.get(); - for (Brightness b : brightnessMap.values()) - if (maxBrightness < b.brightness) maxBrightness = b.brightness; - brightness.putAll(future.get()); - } catch (Exception e) { - logger.error(e.getLocalizedMessage(), e); - } - } - executor.shutdown(); - } - - /*- - double[] intersectPoint = new double[3]; - - Map brightness = new HashMap<>(); - - double[] cameraXYZArray = cameraXYZ.toArray(); - for (int i = 0; i < xPixels; i++) { - for (int j = 0; j < yPixels; j++) { - Vector3D pixelDir = pixelToBodyFixed(((double) i) / scale, ((double) j) / scale); - - int intersect = - sbm.computeRayIntersection(cameraXYZArray, pixelDir.toArray(), intersectPoint); - if (intersect > 0) { - brightness.put(i * xPixels + j, - getBrightness(pf, intersect, new Vector3D(intersectPoint))); - } - } - } - */ - if (brightness.isEmpty()) { - logger.info("No intersections with shape model found!"); - } else { - for (int j = 0; j < yPixels; j++) { - for (int i = 0; i < xPixels; i++) { - if (!brightness.containsKey(j * xPixels + i)) continue; - double value = brightness.get(j * xPixels + i).brightness; - - int grey = value < 0.01 ? 1 : (int) (255 * value / maxBrightness); - - image.setRGB(i, j, new Color(grey, grey, grey).getRGB()); - } - } - } - - BufferedImage img = new BufferedImage(nPixelsX, nPixelsY, BufferedImage.TYPE_INT_RGB); - img.createGraphics() - .drawImage(image.getScaledInstance(nPixelsX, nPixelsY, Image.SCALE_SMOOTH), 0, 0, null); - - return img; - } - - public SumFile loadSumFile(String filename) { - - SumFile sumFile = SumFile.fromFile(new File(filename)); - - addMetaData("image.utc", "Imaging date. Taken from sumfile", sumFile.utcString()); - - nPixelsX = sumFile.imageWidth(); - nPixelsY = sumFile.imageHeight(); - - centerX = 0.5 * (nPixelsX - 1); - centerY = 0.5 * (nPixelsY - 1); - - // let's assume square pixels - ifov = sumFile.horizontalResolution(); - - // put the sun far away - sunXYZ = sumFile.sunDirection().scalarMultiply(1e8); - cameraXYZ = sumFile.scobj().negate(); - - cameraToBodyFixed = sumFile.getBodyFixedToCamera().revert(); - - double[] intersectPoint = new double[3]; - Vector3D boresight = sumFile.boresight(); - long intersect = - getGlobalModel() - .computeRayIntersection(cameraXYZ.toArray(), boresight.toArray(), intersectPoint); - if (intersect > 0) { - Vector3D nadirPt = new Vector3D(intersectPoint); - double lat = nadirPt.getDelta(); - double lon = nadirPt.getAlpha(); - Vector3D normal = new Vector3D(getGlobalModel().getCellNormals().GetTuple3(intersect)); - double inc = Vector3D.angle(sunXYZ, normal); - Vector3D toCamera = cameraXYZ.subtract(nadirPt); - double ems = Vector3D.angle(toCamera, normal); - double phs = Vector3D.angle(sunXYZ, toCamera); - double range = cameraXYZ.subtract(nadirPt).getNorm(); - - addMetaData("image.cell", "Index of center pixel cell", Long.toString(intersect)); - addMetaData("image.lat", "Center latitude", StringFunctions.toDegreesLat("%.2f ").apply(lat)); - addMetaData( - "image.lon", "Center longitude", StringFunctions.toDegreesELon("%.2f ").apply(lon)); - addMetaData( - "image.inc", "Center incidence in degrees", String.format("%.2f", Math.toDegrees(inc))); - addMetaData( - "image.ems", - "Center emission in degrees (may not be zero if facet is tilted)", - String.format("%.2f", Math.toDegrees(ems))); - addMetaData( - "image.phs", "Center phase in degrees", String.format("%.2f", Math.toDegrees(phs))); - addMetaData("image.range", "Center point range in m", String.format("%.3f", range * 1e3)); - addMetaData( - "image.resolution", - "Center point resolution in m/pixel", - String.format("%.3f", ifov * range * 1e3)); - } - return sumFile; - } - - /** - * load a local model file - * - * @param lmcName local model filename - */ - public void loadLocalModels(String lmcName) { - LocalModelCollection lmc = new LocalModelCollection(128, scale, rotation); - try { - List lines = FileUtils.readLines(new File(lmcName), Charset.defaultCharset()); - - Double localResolution = null; - - for (String line : lines) { - String strippedLine = line.strip(); - if (strippedLine.isEmpty() || strippedLine.startsWith("#")) continue; - String[] parts = strippedLine.split(","); - - if (localResolution == null) { - localResolution = Double.valueOf(parts[0].strip()); - logger.debug("Loading {} with a resolution of {} m/pixel", lmcName, localResolution); + */ + if (brightness.isEmpty()) { + logger.info("No intersections with shape model found!"); } else { - double lat = Math.toRadians(Double.parseDouble(parts[0])); - double lon = Math.toRadians(Double.parseDouble(parts[1])); - String filename = parts[2].strip(); - lmc.addModel(lat, lon, filename); + for (int j = 0; j < yPixels; j++) { + for (int i = 0; i < xPixels; i++) { + if (!brightness.containsKey(j * xPixels + i)) continue; + double value = brightness.get(j * xPixels + i).brightness; + + int grey = value < 0.01 ? 1 : (int) (255 * value / maxBrightness); + + image.setRGB(i, j, new Color(grey, grey, grey).getRGB()); + } + } } - } - this.lmcMap.put(localResolution, lmc); - } catch (IOException e) { - logger.error(e.getLocalizedMessage()); + + BufferedImage img = new BufferedImage(nPixelsX, nPixelsY, BufferedImage.TYPE_INT_RGB); + img.createGraphics().drawImage(image.getScaledInstance(nPixelsX, nPixelsY, Image.SCALE_SMOOTH), 0, 0, null); + + return img; } - } - /** - * Write a metadata file containing information about the simulated image. - * - * @param metadataFile file to write - * @param arguments command line arguments - */ - public void writeMetadata(File metadataFile, String arguments) { - try (PrintWriter pw = new PrintWriter(metadataFile)) { + public SumFile loadSumFile(String filename) { - pw.printf("# Created %s by %s\n", Instant.now().toString(), AppVersion.getVersionString()); - pw.printf("# arguments: %s\n\n", arguments); + SumFile sumFile = SumFile.fromFile(new File(filename)); - String lastSection = null; - for (String key : metadata.keySet()) { - String thisSection = key.substring(0, key.indexOf(".")); - if (lastSection == null) lastSection = thisSection; - if (!lastSection.equals(thisSection)) { - pw.println(); - lastSection = thisSection; + addMetaData("image.utc", "Imaging date. Taken from sumfile", sumFile.utcString()); + + nPixelsX = sumFile.imageWidth(); + nPixelsY = sumFile.imageHeight(); + + centerX = 0.5 * (nPixelsX - 1); + centerY = 0.5 * (nPixelsY - 1); + + // let's assume square pixels + ifov = sumFile.horizontalResolution(); + + // put the sun far away + sunXYZ = sumFile.sunDirection().scalarMultiply(1e8); + cameraXYZ = sumFile.scobj().negate(); + + cameraToBodyFixed = sumFile.getBodyFixedToCamera().revert(); + + double[] intersectPoint = new double[3]; + Vector3D boresight = sumFile.boresight(); + long intersect = + getGlobalModel().computeRayIntersection(cameraXYZ.toArray(), boresight.toArray(), intersectPoint); + if (intersect > 0) { + Vector3D nadirPt = new Vector3D(intersectPoint); + double lat = nadirPt.getDelta(); + double lon = nadirPt.getAlpha(); + Vector3D normal = new Vector3D(getGlobalModel().getCellNormals().GetTuple3(intersect)); + double inc = Vector3D.angle(sunXYZ, normal); + Vector3D toCamera = cameraXYZ.subtract(nadirPt); + double ems = Vector3D.angle(toCamera, normal); + double phs = Vector3D.angle(sunXYZ, toCamera); + double range = cameraXYZ.subtract(nadirPt).getNorm(); + + addMetaData("image.cell", "Index of center pixel cell", Long.toString(intersect)); + addMetaData( + "image.lat", + "Center latitude", + StringFunctions.toDegreesLat("%.2f ").apply(lat)); + addMetaData( + "image.lon", + "Center longitude", + StringFunctions.toDegreesELon("%.2f ").apply(lon)); + addMetaData("image.inc", "Center incidence in degrees", String.format("%.2f", Math.toDegrees(inc))); + addMetaData( + "image.ems", + "Center emission in degrees (may not be zero if facet is tilted)", + String.format("%.2f", Math.toDegrees(ems))); + addMetaData("image.phs", "Center phase in degrees", String.format("%.2f", Math.toDegrees(phs))); + addMetaData("image.range", "Center point range in m", String.format("%.3f", range * 1e3)); + addMetaData( + "image.resolution", + "Center point resolution in m/pixel", + String.format("%.3f", ifov * range * 1e3)); } - Map.Entry value = metadata.get(key); - String[] comments = value.getKey().split("\\r?\\n"); - for (String comment : comments) if (!comment.trim().isEmpty()) pw.printf("# %s\n", comment); - pw.printf("%s = %s\n", key, value.getValue()); - } - - } catch (FileNotFoundException e) { - logger.log(Level.ERROR, e.getLocalizedMessage(), e); + return sumFile; } - } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("albedoFile") - .hasArg() - .desc( - """ + /** + * load a local model file + * + * @param lmcName local model filename + */ + public void loadLocalModels(String lmcName) { + LocalModelCollection lmc = new LocalModelCollection(128, scale, rotation); + try { + List lines = FileUtils.readLines(new File(lmcName), Charset.defaultCharset()); + + Double localResolution = null; + + for (String line : lines) { + String strippedLine = line.strip(); + if (strippedLine.isEmpty() || strippedLine.startsWith("#")) continue; + String[] parts = strippedLine.split(","); + + if (localResolution == null) { + localResolution = Double.valueOf(parts[0].strip()); + logger.debug("Loading {} with a resolution of {} m/pixel", lmcName, localResolution); + } else { + double lat = Math.toRadians(Double.parseDouble(parts[0])); + double lon = Math.toRadians(Double.parseDouble(parts[1])); + String filename = parts[2].strip(); + lmc.addModel(lat, lon, filename); + } + } + this.lmcMap.put(localResolution, lmc); + } catch (IOException e) { + logger.error(e.getLocalizedMessage()); + } + } + + /** + * Write a metadata file containing information about the simulated image. + * + * @param metadataFile file to write + * @param arguments command line arguments + */ + public void writeMetadata(File metadataFile, String arguments) { + try (PrintWriter pw = new PrintWriter(metadataFile)) { + + pw.printf("# Created %s by %s\n", Instant.now().toString(), AppVersion.getVersionString()); + pw.printf("# arguments: %s\n\n", arguments); + + String lastSection = null; + for (String key : metadata.keySet()) { + String thisSection = key.substring(0, key.indexOf(".")); + if (lastSection == null) lastSection = thisSection; + if (!lastSection.equals(thisSection)) { + pw.println(); + lastSection = thisSection; + } + Map.Entry value = metadata.get(key); + String[] comments = value.getKey().split("\\r?\\n"); + for (String comment : comments) if (!comment.trim().isEmpty()) pw.printf("# %s\n", comment); + pw.printf("%s = %s\n", key, value.getValue()); + } + + } catch (FileNotFoundException e) { + logger.log(Level.ERROR, e.getLocalizedMessage(), e); + } + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("albedoFile") + .hasArg() + .desc( + """ Name of albedo file. This is a CSV file with facet index in the first column and albedo (in the range 0 to 1) in the second. Additional columns are @@ -671,14 +665,13 @@ public class RenderShapeFromSumFile implements TerrasaurTool { set to 1. Lines starting with # or blank lines are ignored. This file applies only to the default shape model, not any local ones.""" - .replaceAll("\\s+", " ") - .strip()) - .build()); - options.addOption( - Option.builder("localModels") - .hasArgs() - .desc( - """ + .replaceAll("\\s+", " ") + .strip()) + .build()); + options.addOption(Option.builder("localModels") + .hasArgs() + .desc( + """ File containing local shape models, one per line. The first line of the file should contain the coarsest resolution in m/pixel where these models @@ -692,173 +685,155 @@ public class RenderShapeFromSumFile implements TerrasaurTool { may be specified multiple times to load multiple sets of models. Lines starting with # or blank lines are ignored.""" - .replaceAll("\\s+", " ") - .strip()) - .build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("model") - .required() - .hasArg() - .desc( - "Required. Default shape model filename. Supported formats are OBJ, PLT, PLY, STL, or VTK format.") - .build()); - options.addOption( - Option.builder("numThreads") - .hasArg() - .desc("Number of threads to run in parallel when generating the image. Default is 2.") - .build()); - options.addOption( - Option.builder("photo") - .hasArg() - .desc(PhotometricFunction.getOptionString().trim() + " Default is OREX.") - .build()); - options.addOption( - Option.builder("output") - .required() - .hasArg() - .desc("Required. Name of image file to write. Valid extensions are fits or png.") - .build()); - options.addOption( - Option.builder("rotateModel") - .hasArg() - .desc( - """ + .replaceAll("\\s+", " ") + .strip()) + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("model") + .required() + .hasArg() + .desc( + "Required. Default shape model filename. Supported formats are OBJ, PLT, PLY, STL, or VTK format.") + .build()); + options.addOption(Option.builder("numThreads") + .hasArg() + .desc("Number of threads to run in parallel when generating the image. Default is 2.") + .build()); + options.addOption(Option.builder("photo") + .hasArg() + .desc(PhotometricFunction.getOptionString().trim() + " Default is OREX.") + .build()); + options.addOption(Option.builder("output") + .required() + .hasArg() + .desc("Required. Name of image file to write. Valid extensions are fits or png.") + .build()); + options.addOption(Option.builder("rotateModel") + .hasArg() + .desc( + """ If present, rotate shape model. Specify by an angle (degrees) and a 3 element rotation axis vector (XYZ) separated by commas. """ - .replaceAll("\\s+", " ") - .strip()) - .build()); - options.addOption( - Option.builder("scaleModel") - .hasArg() - .desc("If present, factor to scale shape model. The center is unchanged.") - .build()); - options.addOption( - Option.builder("subPixel") - .hasArg() - .desc( - """ + .replaceAll("\\s+", " ") + .strip()) + .build()); + options.addOption(Option.builder("scaleModel") + .hasArg() + .desc("If present, factor to scale shape model. The center is unchanged.") + .build()); + options.addOption(Option.builder("subPixel") + .hasArg() + .desc( + """ Generate the simulated image a factor of subPixel (must be an integer) larger than the dimensions in the sum file. The simulated image is them reduced in size to the dimensions in the sum file. The default is 2. """ - .replaceAll("\\s+", " ") - .strip()) - .build()); - options.addOption( - Option.builder("sumFile") - .required() - .hasArg() - .desc("Required. Name of sum file to read.") - .build()); - return options; - } - - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new RenderShapeFromSumFile(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadVtkLibraries(); - - Double scale = - cl.hasOption("scaleModel") ? Double.parseDouble(cl.getOptionValue("scaleModel")) : null; - Rotation rotation = - cl.hasOption("rotateModel") - ? RotationUtils.stringToRotation(cl.getOptionValue("rotateModel")) - : null; - - RenderShapeFromSumFile app = - new RenderShapeFromSumFile(cl.getOptionValue("model"), scale, rotation); - SumFile sumFile = app.loadSumFile(cl.getOptionValue("sumFile")); - - if (cl.hasOption("albedoFile")) app.loadAlbedoFile(cl.getOptionValue("albedoFile")); - - if (cl.hasOption("localModels")) - for (String localModel : cl.getOptionValues("localModels")) app.loadLocalModels(localModel); - - if (cl.hasOption("subPixel")) app.setSubPixel(Integer.parseInt(cl.getOptionValue("subPixel"))); - - PhotometricFunction pf = PhotometricFunction.OREX1; - if (cl.hasOption("photo")) - pf = PhotometricFunction.getPhotometricFunction(cl.getOptionValue("photo")); - int numThreads = - cl.hasOption("numThreads") ? Integer.parseInt(cl.getOptionValue("numThreads")) : 2; - - String outputFilename = cl.getOptionValue("output"); - String dirname = FilenameUtils.getPath(outputFilename); - if (dirname.trim().isEmpty()) dirname = "."; - String basename = FilenameUtils.getBaseName(outputFilename); - String extension = FilenameUtils.getExtension(outputFilename); - - if (extension.equalsIgnoreCase("png")) { - BufferedImage image = app.getImage(pf, numThreads); - File png = new File(dirname, basename + "." + extension); - File metadataFile = new File(dirname, basename + ".txt"); - - ImageIO.write(image, "PNG", png); - app.writeMetadata(metadataFile, startupMessages.get(MessageLabel.ARGUMENTS)); - logger.info("Wrote {}", outputFilename); - } else if (extension.equalsIgnoreCase("fits")) { - Fits fits = new Fits(); - ImageHDU imageHDU = (ImageHDU) Fits.makeHDU(app.getFits(pf, numThreads)); - Header header = imageHDU.getHeader(); - header.addValue( - DateTime.TIMESYS_UTC, app.metadata.get("image.utc").getValue(), "Time from the SUM file"); - header.addValue("TITLE", sumFile.picnm(), "Title of SUM file"); - header.addValue("PLANE1", "brightness", "from 0 to 1"); - header.addValue("PLANE2", "incidence", "degrees"); - header.addValue("PLANE3", "emission", "degrees"); - header.addValue("PLANE4", "phase", "degrees"); - header.addValue("PLANE5", "range", "kilometers"); - header.addValue("PLANE6", "facetX", "kilometers"); - header.addValue("PLANE7", "facetY", "kilometers"); - header.addValue("PLANE8", "facetZ", "kilometers"); - header.addValue("PLANE9", "normalX", "X component of unit normal"); - header.addValue("PLANE10", "normalY", "Y component of unit normal"); - header.addValue("PLANE11", "normalZ", "Z component of unit normal"); - header.addValue("MMFL", sumFile.mmfl(), "From SUM file"); - header.addValue("SCOBJ", sumFile.scobj().toString(), "From SUM file"); - header.addValue("CX", sumFile.cx().toString(), "From SUM file"); - header.addValue("CY", sumFile.cy().toString(), "From SUM file"); - header.addValue("CZ", sumFile.cz().toString(), "From SUM file"); - header.addValue("SZ", sumFile.sz().toString(), "From SUM file"); - header.addValue("KMAT1", sumFile.kmat1().toString(), "From SUM file"); - header.addValue("KMAT2", sumFile.kmat2().toString(), "From SUM file"); - header.addValue("DIST", sumFile.distortion().toString(), "From SUM file"); - header.addValue("SIGVSO", sumFile.sig_vso().toString(), "From SUM file"); - header.addValue("SIGPTG", sumFile.sig_ptg().toString(), "From SUM file"); - fits.addHDU(imageHDU); - fits.write(outputFilename); - fits.close(); - logger.info("wrote {}", outputFilename); - } else { - logger.error("Unsupported output file type: {}", outputFilename); + .replaceAll("\\s+", " ") + .strip()) + .build()); + options.addOption(Option.builder("sumFile") + .required() + .hasArg() + .desc("Required. Name of sum file to read.") + .build()); + return options; + } + + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new RenderShapeFromSumFile(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + NativeLibraryLoader.loadVtkLibraries(); + + Double scale = cl.hasOption("scaleModel") ? Double.parseDouble(cl.getOptionValue("scaleModel")) : null; + Rotation rotation = + cl.hasOption("rotateModel") ? RotationUtils.stringToRotation(cl.getOptionValue("rotateModel")) : null; + + RenderShapeFromSumFile app = new RenderShapeFromSumFile(cl.getOptionValue("model"), scale, rotation); + SumFile sumFile = app.loadSumFile(cl.getOptionValue("sumFile")); + + if (cl.hasOption("albedoFile")) app.loadAlbedoFile(cl.getOptionValue("albedoFile")); + + if (cl.hasOption("localModels")) + for (String localModel : cl.getOptionValues("localModels")) app.loadLocalModels(localModel); + + if (cl.hasOption("subPixel")) app.setSubPixel(Integer.parseInt(cl.getOptionValue("subPixel"))); + + PhotometricFunction pf = PhotometricFunction.OREX1; + if (cl.hasOption("photo")) pf = PhotometricFunction.getPhotometricFunction(cl.getOptionValue("photo")); + int numThreads = cl.hasOption("numThreads") ? Integer.parseInt(cl.getOptionValue("numThreads")) : 2; + + String outputFilename = cl.getOptionValue("output"); + String dirname = FilenameUtils.getPath(outputFilename); + if (dirname.trim().isEmpty()) dirname = "."; + String basename = FilenameUtils.getBaseName(outputFilename); + String extension = FilenameUtils.getExtension(outputFilename); + + if (extension.equalsIgnoreCase("png")) { + BufferedImage image = app.getImage(pf, numThreads); + File png = new File(dirname, basename + "." + extension); + File metadataFile = new File(dirname, basename + ".txt"); + + ImageIO.write(image, "PNG", png); + app.writeMetadata(metadataFile, startupMessages.get(MessageLabel.ARGUMENTS)); + logger.info("Wrote {}", outputFilename); + } else if (extension.equalsIgnoreCase("fits")) { + Fits fits = new Fits(); + ImageHDU imageHDU = (ImageHDU) Fits.makeHDU(app.getFits(pf, numThreads)); + Header header = imageHDU.getHeader(); + header.addValue(DateTime.TIMESYS_UTC, app.metadata.get("image.utc").getValue(), "Time from the SUM file"); + header.addValue("TITLE", sumFile.picnm(), "Title of SUM file"); + header.addValue("PLANE1", "brightness", "from 0 to 1"); + header.addValue("PLANE2", "incidence", "degrees"); + header.addValue("PLANE3", "emission", "degrees"); + header.addValue("PLANE4", "phase", "degrees"); + header.addValue("PLANE5", "range", "kilometers"); + header.addValue("PLANE6", "facetX", "kilometers"); + header.addValue("PLANE7", "facetY", "kilometers"); + header.addValue("PLANE8", "facetZ", "kilometers"); + header.addValue("PLANE9", "normalX", "X component of unit normal"); + header.addValue("PLANE10", "normalY", "Y component of unit normal"); + header.addValue("PLANE11", "normalZ", "Z component of unit normal"); + header.addValue("MMFL", sumFile.mmfl(), "From SUM file"); + header.addValue("SCOBJ", sumFile.scobj().toString(), "From SUM file"); + header.addValue("CX", sumFile.cx().toString(), "From SUM file"); + header.addValue("CY", sumFile.cy().toString(), "From SUM file"); + header.addValue("CZ", sumFile.cz().toString(), "From SUM file"); + header.addValue("SZ", sumFile.sz().toString(), "From SUM file"); + header.addValue("KMAT1", sumFile.kmat1().toString(), "From SUM file"); + header.addValue("KMAT2", sumFile.kmat2().toString(), "From SUM file"); + header.addValue("DIST", sumFile.distortion().toString(), "From SUM file"); + header.addValue("SIGVSO", sumFile.sig_vso().toString(), "From SUM file"); + header.addValue("SIGPTG", sumFile.sig_ptg().toString(), "From SUM file"); + fits.addHDU(imageHDU); + fits.write(outputFilename); + fits.close(); + logger.info("wrote {}", outputFilename); + } else { + logger.error("Unsupported output file type: {}", outputFilename); + } } - } } diff --git a/src/main/java/terrasaur/apps/RotationConversion.java b/src/main/java/terrasaur/apps/RotationConversion.java index bcbe8a7..6228fac 100644 --- a/src/main/java/terrasaur/apps/RotationConversion.java +++ b/src/main/java/terrasaur/apps/RotationConversion.java @@ -42,201 +42,228 @@ import terrasaur.templates.TerrasaurTool; public class RotationConversion implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Convert rotations between different types."; - } + @Override + public String shortDescription() { + return "Convert rotations between different types."; + } - @Override - public String fullDescription(Options options) { + @Override + public String fullDescription(Options options) { - String header = ""; - String footer = - """ + String header = ""; + String footer = + """ This program converts rotations between angle and axis, 3x3 matrix, quaternions, \ and ZXZ rotation Euler angles. Note that the rotation modifies the frame; \ the vector is considered to be fixed. To find the rotation that modifies the \ vector in a fixed frame, take the transpose of this matrix. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - - } - - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("logFile").hasArg() - .desc("If present, save screen output to log file.").build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) - sb.append(String.format("%s ", l.name())); - options.addOption(Option.builder("logLevel").hasArg() - .desc("If present, print messages above selected priority. Valid values are " - + sb.toString().trim() + ". Default is INFO.") - .build()); - - options.addOption(Option.builder("angle").hasArg().desc("Rotation angle, in radians.").build()); - options.addOption( - Option.builder("axis0").hasArg().desc("First element of rotation axis.").build()); - options.addOption( - Option.builder("axis1").hasArg().desc("Second element of rotation axis.").build()); - options.addOption( - Option.builder("axis2").hasArg().desc("Third element of rotation axis.").build()); - options.addOption(Option.builder("cardanXYZ1").hasArg() - .desc("Cardan angle for the first rotation (about the X axis) in radians.").build()); - options.addOption(Option.builder("cardanXYZ2").hasArg() - .desc("Cardan angle for the second rotation (about the Y axis) in radians.").build()); - options.addOption(Option.builder("cardanXYZ3").hasArg() - .desc("Cardan angle for the third rotation (about the Z axis) in radians.").build()); - options.addOption(Option.builder("eulerZXZ1").hasArg() - .desc("Euler angle for the first rotation (about the Z axis) in radians.").build()); - options.addOption(Option.builder("eulerZXZ2").hasArg() - .desc("Euler angle for the second rotation (about the rotated X axis) in radians.") - .build()); - options.addOption(Option.builder("eulerZXZ3").hasArg() - .desc("Euler angle for the third rotation (about the rotated Z axis) in radians.").build()); - options.addOption( - Option.builder("q0").hasArg().desc("Scalar term for quaternion: cos(theta/2)").build()); - options.addOption(Option.builder("q1").hasArg() - .desc("First vector term for quaternion: sin(theta/2) * V[0]").build()); - options.addOption(Option.builder("q2").hasArg() - .desc("Second vector term for quaternion: sin(theta/2) * V[1]").build()); - options.addOption(Option.builder("q3").hasArg() - .desc("Third vector term for quaternion: sin(theta/2) * V[2]").build()); - options.addOption(Option.builder("matrix").hasArg() - .desc("name of file containing rotation matrix to convert to Euler angles. " - + "Format is 3x3 array in plain text separated by white space.") - .build()); - options.addOption(Option.builder("anglesInDegrees").desc( - "If present, input angles in degrees and print output angles in degrees. Default is false.") - .build()); return options; - } - - public static void main(String[] args) throws Exception { - RotationConversion defaultOBJ = new RotationConversion(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - boolean inDegrees = cl.hasOption("anglesInDegrees"); - - boolean axisAndAngle = cl.hasOption("angle") && cl.hasOption("axis0") && cl.hasOption("axis1") - && cl.hasOption("axis2"); - boolean cardanXYZ = - cl.hasOption("cardanXYZ1") && cl.hasOption("cardanXYZ2") && cl.hasOption("cardanXYZ3"); - boolean eulerZXZ = - cl.hasOption("eulerZXZ1") && cl.hasOption("eulerZXZ3") && cl.hasOption("eulerZXZ3"); - boolean quaternion = - cl.hasOption("q0") && cl.hasOption("q1") && cl.hasOption("q2") && cl.hasOption("q3"); - boolean matrix = cl.hasOption("matrix"); - - if (!(axisAndAngle || cardanXYZ || eulerZXZ || quaternion || matrix)) { - logger.warn( - "Must specify input rotation as axis and angle, Cardan or Euler angles, matrix, or quaternion."); - System.exit(0); + return TerrasaurTool.super.fullDescription(options, header, footer); } - Rotation r = null; - if (matrix) { - List lines = - FileUtils.readLines(new File(cl.getOptionValue("matrix")), Charset.defaultCharset()); - double[][] m = new double[3][3]; - for (int i = 0; i < 3; i++) { - String[] parts = lines.get(i).trim().split("\\s+"); - for (int j = 0; j < 3; j++) - m[i][j] = Double.parseDouble(parts[j].trim()); - } - r = new Rotation(m, 1e-10); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + ". Default is INFO.") + .build()); + + options.addOption(Option.builder("angle") + .hasArg() + .desc("Rotation angle, in radians.") + .build()); + options.addOption(Option.builder("axis0") + .hasArg() + .desc("First element of rotation axis.") + .build()); + options.addOption(Option.builder("axis1") + .hasArg() + .desc("Second element of rotation axis.") + .build()); + options.addOption(Option.builder("axis2") + .hasArg() + .desc("Third element of rotation axis.") + .build()); + options.addOption(Option.builder("cardanXYZ1") + .hasArg() + .desc("Cardan angle for the first rotation (about the X axis) in radians.") + .build()); + options.addOption(Option.builder("cardanXYZ2") + .hasArg() + .desc("Cardan angle for the second rotation (about the Y axis) in radians.") + .build()); + options.addOption(Option.builder("cardanXYZ3") + .hasArg() + .desc("Cardan angle for the third rotation (about the Z axis) in radians.") + .build()); + options.addOption(Option.builder("eulerZXZ1") + .hasArg() + .desc("Euler angle for the first rotation (about the Z axis) in radians.") + .build()); + options.addOption(Option.builder("eulerZXZ2") + .hasArg() + .desc("Euler angle for the second rotation (about the rotated X axis) in radians.") + .build()); + options.addOption(Option.builder("eulerZXZ3") + .hasArg() + .desc("Euler angle for the third rotation (about the rotated Z axis) in radians.") + .build()); + options.addOption(Option.builder("q0") + .hasArg() + .desc("Scalar term for quaternion: cos(theta/2)") + .build()); + options.addOption(Option.builder("q1") + .hasArg() + .desc("First vector term for quaternion: sin(theta/2) * V[0]") + .build()); + options.addOption(Option.builder("q2") + .hasArg() + .desc("Second vector term for quaternion: sin(theta/2) * V[1]") + .build()); + options.addOption(Option.builder("q3") + .hasArg() + .desc("Third vector term for quaternion: sin(theta/2) * V[2]") + .build()); + options.addOption(Option.builder("matrix") + .hasArg() + .desc("name of file containing rotation matrix to convert to Euler angles. " + + "Format is 3x3 array in plain text separated by white space.") + .build()); + options.addOption(Option.builder("anglesInDegrees") + .desc("If present, input angles in degrees and print output angles in degrees. Default is false.") + .build()); + return options; } - if (axisAndAngle) { - double angle = Double.parseDouble(cl.getOptionValue("angle").trim()); - if (inDegrees) - angle = Math.toRadians(angle); - r = new Rotation( - new Vector3D(Double.parseDouble(cl.getOptionValue("axis0").trim()), - Double.parseDouble(cl.getOptionValue("axis1").trim()), - Double.parseDouble(cl.getOptionValue("axis2").trim())), - angle, RotationConvention.FRAME_TRANSFORM); + public static void main(String[] args) throws Exception { + RotationConversion defaultOBJ = new RotationConversion(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + boolean inDegrees = cl.hasOption("anglesInDegrees"); + + boolean axisAndAngle = + cl.hasOption("angle") && cl.hasOption("axis0") && cl.hasOption("axis1") && cl.hasOption("axis2"); + boolean cardanXYZ = cl.hasOption("cardanXYZ1") && cl.hasOption("cardanXYZ2") && cl.hasOption("cardanXYZ3"); + boolean eulerZXZ = cl.hasOption("eulerZXZ1") && cl.hasOption("eulerZXZ3") && cl.hasOption("eulerZXZ3"); + boolean quaternion = cl.hasOption("q0") && cl.hasOption("q1") && cl.hasOption("q2") && cl.hasOption("q3"); + boolean matrix = cl.hasOption("matrix"); + + if (!(axisAndAngle || cardanXYZ || eulerZXZ || quaternion || matrix)) { + logger.warn( + "Must specify input rotation as axis and angle, Cardan or Euler angles, matrix, or quaternion."); + System.exit(0); + } + + Rotation r = null; + if (matrix) { + List lines = FileUtils.readLines(new File(cl.getOptionValue("matrix")), Charset.defaultCharset()); + double[][] m = new double[3][3]; + for (int i = 0; i < 3; i++) { + String[] parts = lines.get(i).trim().split("\\s+"); + for (int j = 0; j < 3; j++) m[i][j] = Double.parseDouble(parts[j].trim()); + } + r = new Rotation(m, 1e-10); + } + + if (axisAndAngle) { + double angle = Double.parseDouble(cl.getOptionValue("angle").trim()); + if (inDegrees) angle = Math.toRadians(angle); + r = new Rotation( + new Vector3D( + Double.parseDouble(cl.getOptionValue("axis0").trim()), + Double.parseDouble(cl.getOptionValue("axis1").trim()), + Double.parseDouble(cl.getOptionValue("axis2").trim())), + angle, + RotationConvention.FRAME_TRANSFORM); + } + + if (cardanXYZ) { + double angle1 = Double.parseDouble(cl.getOptionValue("cardanXYZ1").trim()); + double angle2 = Double.parseDouble(cl.getOptionValue("cardanXYZ2").trim()); + double angle3 = Double.parseDouble(cl.getOptionValue("cardanXYZ3").trim()); + if (inDegrees) { + angle1 = Math.toRadians(angle1); + angle2 = Math.toRadians(angle2); + angle3 = Math.toRadians(angle3); + } + r = new Rotation(RotationOrder.XYZ, RotationConvention.FRAME_TRANSFORM, angle1, angle2, angle3); + } + + if (eulerZXZ) { + double angle1 = Double.parseDouble(cl.getOptionValue("eulerZXZ1").trim()); + double angle2 = Double.parseDouble(cl.getOptionValue("eulerZXZ2").trim()); + double angle3 = Double.parseDouble(cl.getOptionValue("eulerZXZ3").trim()); + if (inDegrees) { + angle1 = Math.toRadians(angle1); + angle2 = Math.toRadians(angle2); + angle3 = Math.toRadians(angle3); + } + r = new Rotation(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM, angle1, angle2, angle3); + } + + if (quaternion) { + r = new Rotation( + Double.parseDouble(cl.getOptionValue("q0").trim()), + Double.parseDouble(cl.getOptionValue("q1").trim()), + Double.parseDouble(cl.getOptionValue("q2").trim()), + Double.parseDouble(cl.getOptionValue("q3").trim()), + true); + } + + double[][] m = r.getMatrix(); + String matrixString = String.format( + "rotation matrix:\n%24.16e %24.16e %24.16e\n%24.16e %24.16e %24.16e\n%24.16e %24.16e %24.16e", + m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2]); + System.out.println(matrixString); + + String axisAndAngleString = inDegrees + ? String.format( + "angle (degrees), axis:\n%g, %s", + Math.toDegrees(r.getAngle()), r.getAxis(RotationConvention.FRAME_TRANSFORM)) + : String.format( + "angle (radians), axis:\n%g, %s", r.getAngle(), r.getAxis(RotationConvention.FRAME_TRANSFORM)); + System.out.println(axisAndAngleString); + + try { + double[] angles = r.getAngles(RotationOrder.XYZ, RotationConvention.FRAME_TRANSFORM); + String cardanString = inDegrees + ? String.format( + "Cardan XYZ angles (degrees):\n%g, %g, %g", + Math.toDegrees(angles[0]), Math.toDegrees(angles[1]), Math.toDegrees(angles[2])) + : String.format("Cardan XYZ angles (radians):\n%g, %g, %g", angles[0], angles[1], angles[2]); + System.out.println(cardanString); + } catch (CardanEulerSingularityException e) { + System.out.println("Cardan angles: encountered singularity, cannot solve"); + } + + try { + double[] angles = r.getAngles(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM); + String eulerString = inDegrees + ? String.format( + "Euler ZXZ angles (degrees):\n%g, %g, %g", + Math.toDegrees(angles[0]), Math.toDegrees(angles[1]), Math.toDegrees(angles[2])) + : String.format("Euler ZXZ angles (radians):\n%g, %g, %g", angles[0], angles[1], angles[2]); + System.out.println(eulerString); + } catch (CardanEulerSingularityException e) { + System.out.println("Euler angles: encountered singularity, cannot solve"); + } + + System.out.printf("Quaternion:\n%g, %g, %g, %g\n", r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); } - - if (cardanXYZ) { - double angle1 = Double.parseDouble(cl.getOptionValue("cardanXYZ1").trim()); - double angle2 = Double.parseDouble(cl.getOptionValue("cardanXYZ2").trim()); - double angle3 = Double.parseDouble(cl.getOptionValue("cardanXYZ3").trim()); - if (inDegrees) { - angle1 = Math.toRadians(angle1); - angle2 = Math.toRadians(angle2); - angle3 = Math.toRadians(angle3); - } - r = new Rotation(RotationOrder.XYZ, RotationConvention.FRAME_TRANSFORM, angle1, angle2, - angle3); - } - - if (eulerZXZ) { - double angle1 = Double.parseDouble(cl.getOptionValue("eulerZXZ1").trim()); - double angle2 = Double.parseDouble(cl.getOptionValue("eulerZXZ2").trim()); - double angle3 = Double.parseDouble(cl.getOptionValue("eulerZXZ3").trim()); - if (inDegrees) { - angle1 = Math.toRadians(angle1); - angle2 = Math.toRadians(angle2); - angle3 = Math.toRadians(angle3); - } - r = new Rotation(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM, angle1, angle2, - angle3); - } - - if (quaternion) { - r = new Rotation(Double.parseDouble(cl.getOptionValue("q0").trim()), - Double.parseDouble(cl.getOptionValue("q1").trim()), - Double.parseDouble(cl.getOptionValue("q2").trim()), - Double.parseDouble(cl.getOptionValue("q3").trim()), true); - } - - double[][] m = r.getMatrix(); - String matrixString = String.format( - "rotation matrix:\n%24.16e %24.16e %24.16e\n%24.16e %24.16e %24.16e\n%24.16e %24.16e %24.16e", - m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2]); - System.out.println(matrixString); - - String axisAndAngleString = inDegrees - ? String.format("angle (degrees), axis:\n%g, %s", Math.toDegrees(r.getAngle()), - r.getAxis(RotationConvention.FRAME_TRANSFORM)) - : String.format("angle (radians), axis:\n%g, %s", r.getAngle(), - r.getAxis(RotationConvention.FRAME_TRANSFORM)); - System.out.println(axisAndAngleString); - - try { - double[] angles = r.getAngles(RotationOrder.XYZ, RotationConvention.FRAME_TRANSFORM); - String cardanString = inDegrees - ? String.format("Cardan XYZ angles (degrees):\n%g, %g, %g", Math.toDegrees(angles[0]), - Math.toDegrees(angles[1]), Math.toDegrees(angles[2])) - : String.format("Cardan XYZ angles (radians):\n%g, %g, %g", angles[0], angles[1], - angles[2]); - System.out.println(cardanString); - } catch (CardanEulerSingularityException e) { - System.out.println("Cardan angles: encountered singularity, cannot solve"); - } - - try { - double[] angles = r.getAngles(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM); - String eulerString = inDegrees - ? String.format("Euler ZXZ angles (degrees):\n%g, %g, %g", Math.toDegrees(angles[0]), - Math.toDegrees(angles[1]), Math.toDegrees(angles[2])) - : String.format("Euler ZXZ angles (radians):\n%g, %g, %g", angles[0], angles[1], - angles[2]); - System.out.println(eulerString); - } catch (CardanEulerSingularityException e) { - System.out.println("Euler angles: encountered singularity, cannot solve"); - } - - System.out.printf("Quaternion:\n%g, %g, %g, %g\n", r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); - } } diff --git a/src/main/java/terrasaur/apps/SPKFromSumFile.java b/src/main/java/terrasaur/apps/SPKFromSumFile.java index 47b6f41..d8afbb1 100644 --- a/src/main/java/terrasaur/apps/SPKFromSumFile.java +++ b/src/main/java/terrasaur/apps/SPKFromSumFile.java @@ -44,7 +44,7 @@ import terrasaur.utils.math.MathConversions; public class SPKFromSumFile implements TerrasaurTool { - private final static Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); @Override public String shortDescription() { @@ -54,7 +54,8 @@ public class SPKFromSumFile implements TerrasaurTool { @Override public String fullDescription(Options options) { String header = ""; - String footer = """ + String footer = + """ Given three or more sumfiles, fit a parabola to the spacecraft trajectory in J2000 and create an input file for MKSPK. """; @@ -70,10 +71,11 @@ public class SPKFromSumFile implements TerrasaurTool { private NavigableMap sumFilenames; private UnwritableInterval interval; - private SPKFromSumFile(){} + private SPKFromSumFile() {} - private SPKFromSumFile(Body observer, Body target, ReferenceFrame bodyFixed, Map weightMap, - double extend) throws SpiceException { + private SPKFromSumFile( + Body observer, Body target, ReferenceFrame bodyFixed, Map weightMap, double extend) + throws SpiceException { this.observer = observer; this.target = target; this.bodyFixed = bodyFixed; @@ -102,8 +104,9 @@ public class SPKFromSumFile implements TerrasaurTool { * @param velocityIsJ2000 if true, user-supplied velocity is in J2000 frame * @return command to run MKSPK */ - public String writeMKSPKFiles(String basename, List comments, int degree, final Vector3 velocity, - boolean velocityIsJ2000) throws SpiceException { + public String writeMKSPKFiles( + String basename, List comments, int degree, final Vector3 velocity, boolean velocityIsJ2000) + throws SpiceException { String commentFile = basename + "-comments.txt"; String setupFile = basename + ".setup"; @@ -112,23 +115,27 @@ public class SPKFromSumFile implements TerrasaurTool { try (PrintWriter pw = new PrintWriter(commentFile)) { StringBuilder sb = new StringBuilder(); if (!comments.isEmpty()) { - for (String comment : comments) - sb.append(comment).append("\n"); + for (String comment : comments) sb.append(comment).append("\n"); sb.append("\n"); } - sb.append(String.format("This SPK for %s was generated by fitting a parabola to each component of the " + "SCOBJ vector from " + "the following sumfiles:\n", target)); + sb.append(String.format( + "This SPK for %s was generated by fitting a parabola to each component of the " + + "SCOBJ vector from " + "the following sumfiles:\n", + target)); for (String sumFile : sumFilenames.values()) { sb.append(String.format("\t%s\n", sumFile)); } sb.append("The SCOBJ vector was transformed to J2000 and an aberration correction "); - sb.append(String.format("was applied to find the geometric position relative to %s before the parabola " + "fit. ", target.getName())); - sb.append(String.format("The period covered by this SPK is %s to %s.", + sb.append(String.format( + "was applied to find the geometric position relative to %s before the parabola " + "fit. ", + target.getName())); + sb.append(String.format( + "The period covered by this SPK is %s to %s.", new TDBTime(interval.getBegin()).toUTCString("ISOC", 3), new TDBTime(interval.getEnd()).toUTCString("ISOC", 3))); String allComments = sb.toString(); - for (String comment : allComments.split("\\r?\\n")) - pw.println(WordUtils.wrap(comment, 80)); + for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); } catch (FileNotFoundException e) { logger.error(e.getLocalizedMessage(), e); } @@ -196,23 +203,37 @@ public class SPKFromSumFile implements TerrasaurTool { PolynomialFunction zPos = new PolynomialFunction(zCoeff); PolynomialFunction zVel = zPos.polynomialDerivative(); - logger.info("Polynomial fitting coefficients for geometric position of {} relative to {} in J2000:", - observer.getName(), target.getName()); + logger.info( + "Polynomial fitting coefficients for geometric position of {} relative to {} in J2000:", + observer.getName(), + target.getName()); StringBuilder xMsg = new StringBuilder(String.format("X = %e ", xCoeff[0])); StringBuilder yMsg = new StringBuilder(String.format("Y = %e ", yCoeff[0])); StringBuilder zMsg = new StringBuilder(String.format("Z = %e ", zCoeff[0])); for (int i = 1; i <= degree; i++) { - xMsg.append(xCoeff[i] < 0 ? "- " : "+ ").append(String.format("%e ", Math.abs(xCoeff[i]))).append("t").append(i > 1 ? "^" + i : "").append(" "); - yMsg.append(yCoeff[i] < 0 ? "- " : "+ ").append(String.format("%e ", Math.abs(yCoeff[i]))).append("t").append(i > 1 ? "^" + i : "").append(" "); - zMsg.append(zCoeff[i] < 0 ? "- " : "+ ").append(String.format("%e ", Math.abs(zCoeff[i]))).append("t").append(i > 1 ? "^" + i : "").append(" "); + xMsg.append(xCoeff[i] < 0 ? "- " : "+ ") + .append(String.format("%e ", Math.abs(xCoeff[i]))) + .append("t") + .append(i > 1 ? "^" + i : "") + .append(" "); + yMsg.append(yCoeff[i] < 0 ? "- " : "+ ") + .append(String.format("%e ", Math.abs(yCoeff[i]))) + .append("t") + .append(i > 1 ? "^" + i : "") + .append(" "); + zMsg.append(zCoeff[i] < 0 ? "- " : "+ ") + .append(String.format("%e ", Math.abs(zCoeff[i]))) + .append("t") + .append(i > 1 ? "^" + i : "") + .append(" "); } logger.info(xMsg); logger.info(yMsg); logger.info(zMsg); logger.debug(""); - logger.debug("NOTE: comparing aberration correction=LT+S positions from sumfile with aberration " + - "correction=NONE for fit."); + logger.debug("NOTE: comparing aberration correction=LT+S positions from sumfile with aberration " + + "correction=NONE for fit."); for (Double t : sumFiles.keySet()) { TDBTime tdb = new TDBTime(t); SumFile sumFile = sumFiles.get(t); @@ -234,32 +255,34 @@ public class SPKFromSumFile implements TerrasaurTool { double vx = -xVel.value(t); double vy = -yVel.value(t); double vz = -zVel.value(t); - pw.printf("%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", t, -xPos.value(t), -yPos.value(t), - -zPos.value(t), vx, vy, vz); + pw.printf( + "%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", + t, -xPos.value(t), -yPos.value(t), -zPos.value(t), vx, vy, vz); } else { Vector3 thisVelocity = new Vector3(velocity); if (!velocityIsJ2000) { TDBTime tdb = new TDBTime(t); - thisVelocity = bodyFixed.getPositionTransformation(J2000, tdb).mxv(velocity); + thisVelocity = + bodyFixed.getPositionTransformation(J2000, tdb).mxv(velocity); } double vx = thisVelocity.getElt(0); double vy = thisVelocity.getElt(1); double vz = thisVelocity.getElt(2); - pw.printf("%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", t, -xPos.value(t), -yPos.value(t), - -zPos.value(t), vx, vy, vz); + pw.printf( + "%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", + t, -xPos.value(t), -yPos.value(t), -zPos.value(t), vx, vy, vz); } } } catch (FileNotFoundException e) { logger.error(e.getLocalizedMessage(), e); } - try (PrintWriter pw = new PrintWriter(basename + ".csv")) { pw.println("# Note: fit quantities are without light time or aberration corrections"); pw.println("# SCOBJ"); - pw.println("# UTC, TDB, SumFile, SPICE (body fixed) x, y, z, SCOBJ (body fixed) x, y, z, SCOBJ (J2000) x," + - " y, z, SCOBJ (Geometric J2000) x, y, z, Fit SCOBJ (body fixed) x, y, z, Fit SCOBJ (Geometric " + - "J2000) x, y, z"); + pw.println("# UTC, TDB, SumFile, SPICE (body fixed) x, y, z, SCOBJ (body fixed) x, y, z, SCOBJ (J2000) x," + + " y, z, SCOBJ (Geometric J2000) x, y, z, Fit SCOBJ (body fixed) x, y, z, Fit SCOBJ (Geometric " + + "J2000) x, y, z"); for (Double t : sumFiles.keySet()) { SumFile sumFile = sumFiles.get(t); pw.printf("%s,", sumFile.utcString()); @@ -300,8 +323,8 @@ public class SPKFromSumFile implements TerrasaurTool { pw.println(); } pw.println("\n# Velocity"); - pw.println("# UTC, TDB, SumFile, SPICE (body fixed) x, y, z, SPICE (J2000) x, y, z, Fit (body fixed) x, " + - "y, z, Fit (J2000) x, y, z"); + pw.println("# UTC, TDB, SumFile, SPICE (body fixed) x, y, z, SPICE (J2000) x, y, z, Fit (body fixed) x, " + + "y, z, Fit (J2000) x, y, z"); for (Double t : sumFiles.keySet()) { SumFile sumFile = sumFiles.get(t); pw.printf("%s,", sumFile.utcString()); @@ -327,7 +350,8 @@ public class SPKFromSumFile implements TerrasaurTool { if (velocity != null) { Vector3 thisVelocity = new Vector3(velocity); if (!velocityIsJ2000) { - thisVelocity = bodyFixed.getPositionTransformation(J2000, tdb).mxv(velocity); + thisVelocity = + bodyFixed.getPositionTransformation(J2000, tdb).mxv(velocity); } double vx = thisVelocity.getElt(0); double vy = thisVelocity.getElt(1); @@ -335,8 +359,8 @@ public class SPKFromSumFile implements TerrasaurTool { velJ2000 = new Vector3(vx, vy, vz); } - StateVector stateJ2000 = new StateVector(new Vector3(xPos.value(t), yPos.value(t), zPos.value(t)), - velJ2000); + StateVector stateJ2000 = + new StateVector(new Vector3(xPos.value(t), yPos.value(t), zPos.value(t)), velJ2000); velBodyFixed = j2000ToBodyFixed.mxv(stateJ2000).getVector3(1); pw.printf("%s, ", velBodyFixed.getElt(0)); @@ -357,19 +381,39 @@ public class SPKFromSumFile implements TerrasaurTool { private static Options defineOptions() { Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("degree").hasArg().desc("Degree of polynomial used to fit sumFile locations" + "." + " Default is 2.").build()); - options.addOption(Option.builder("extend").hasArg().desc("Extend SPK past the last sumFile by seconds. " - + " Default is zero.").build()); - options.addOption(Option.builder("frame").hasArg().desc("Name of body fixed frame. This will default to the " - + "target's body fixed frame.").build()); - options.addOption(Option.builder("logFile").hasArg().desc("If present, save screen output to log file.").build()); + options.addOption(Option.builder("degree") + .hasArg() + .desc("Degree of polynomial used to fit sumFile locations" + "." + " Default is 2.") + .build()); + options.addOption(Option.builder("extend") + .hasArg() + .desc("Extend SPK past the last sumFile by seconds. " + " Default is zero.") + .build()); + options.addOption(Option.builder("frame") + .hasArg() + .desc("Name of body fixed frame. This will default to the " + "target's body fixed frame.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) - sb.append(String.format("%s ", l.name())); - options.addOption(Option.builder("logLevel").hasArg().desc("If present, print messages above selected " + - "priority. Valid values are " + sb.toString().trim() + ". Default is INFO.").build()); - options.addOption(Option.builder("observer").required().hasArg().desc("Required. SPICE ID for the observer.").build()); - options.addOption(Option.builder("sumFile").hasArg().required().desc(""" + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected " + "priority. Valid values are " + + sb.toString().trim() + ". Default is INFO.") + .build()); + options.addOption(Option.builder("observer") + .required() + .hasArg() + .desc("Required. SPICE ID for the observer.") + .build()); + options.addOption(Option.builder("sumFile") + .hasArg() + .required() + .desc( + """ File listing sumfiles to read. This is a text file, one per line. You can include an optional weight after each filename. The default weight is 1.0. @@ -385,14 +429,31 @@ public class SPKFromSumFile implements TerrasaurTool { D717506131G0.SUM # Weight this last image less than the others D717506132G0.SUM 0.25 - """).build()); - options.addOption(Option.builder("spice").required().hasArgs().desc("Required. SPICE metakernel file " + - "containing body fixed frame and spacecraft kernels. Can specify more than one kernel, separated by " - + "whitespace.").build()); - options.addOption(Option.builder("target").required().hasArg().desc("Required. SPICE ID for the target.").build()); - options.addOption(Option.builder("velocity").hasArgs().desc("Spacecraft velocity relative to target in the " + "body fixed frame. If present, use this fixed velocity in the MKSPK input file. Default is to " + "take the derivative of the fit position. Specify as three floating point values in km/sec," + "separated by whitespace.").build()); - options.addOption(Option.builder("velocityJ2000").desc("If present, argument to -velocity is in J2000 frame. " - + " Ignored if -velocity is not set.").build()); return options; + """) + .build()); + options.addOption(Option.builder("spice") + .required() + .hasArgs() + .desc("Required. SPICE metakernel file " + + "containing body fixed frame and spacecraft kernels. Can specify more than one kernel, separated by " + + "whitespace.") + .build()); + options.addOption(Option.builder("target") + .required() + .hasArg() + .desc("Required. SPICE ID for the target.") + .build()); + options.addOption(Option.builder("velocity") + .hasArgs() + .desc("Spacecraft velocity relative to target in the " + + "body fixed frame. If present, use this fixed velocity in the MKSPK input file. Default is to " + + "take the derivative of the fit position. Specify as three floating point values in km/sec," + + "separated by whitespace.") + .build()); + options.addOption(Option.builder("velocityJ2000") + .desc("If present, argument to -velocity is in J2000 frame. " + " Ignored if -velocity is not set.") + .build()); + return options; } public static void main(String[] args) throws SpiceException { @@ -408,11 +469,9 @@ public class SPKFromSumFile implements TerrasaurTool { NativeLibraryLoader.loadSpiceLibraries(); - final double extend = cl.hasOption("extend") ? Double.parseDouble(cl.getOptionValue("extend")) : 0; - for (String kernel : cl.getOptionValues("spice")) - KernelDatabase.load(kernel); + for (String kernel : cl.getOptionValues("spice")) KernelDatabase.load(kernel); Body observer = new Body(cl.getOptionValue("observer")); Body target = new Body(cl.getOptionValue("target")); @@ -468,5 +527,4 @@ public class SPKFromSumFile implements TerrasaurTool { logger.info("Finished."); } - } diff --git a/src/main/java/terrasaur/apps/ShapeFormatConverter.java b/src/main/java/terrasaur/apps/ShapeFormatConverter.java index 6faacdc..443e8e9 100644 --- a/src/main/java/terrasaur/apps/ShapeFormatConverter.java +++ b/src/main/java/terrasaur/apps/ShapeFormatConverter.java @@ -55,476 +55,452 @@ import vtk.vtkPolyData; public class ShapeFormatConverter implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Transform a shape model to a new coordinate system."; - } - - @Override - public String fullDescription(Options options) { - - String header = ""; - String footer = - "This program will rotate, translate, and/or scale a shape model. It can additionally transform a " - + "single point, a sum file, or an SBMT ellipse file. For a sum file, the SCOBJ vector is " - + "transformed and the cx, cy, cz, and sz vectors are rotated. For SBMT ellipse files, only " - + "center of the ellipse is transformed. The size, orientation, and all other fields in the " - + "file are unchanged."; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private enum COORDTYPE { - LATLON, - XYZ, - POLYDATA - } - - private enum FORMATS { - ICQ, - LLR, - OBJ, - PDS, - PLT, - PLY, - STL, - VTK, - FITS, - SUM, - SBMT - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("centerOfRotation") - .hasArg() - .desc( - "Subtract this point before applying rotation matrix, add back after. " - + "Specify by three floating point numbers separated by commas. If not present default is (0,0,0).") - .build()); - options.addOption( - Option.builder("decimate") - .hasArg() - .desc( - "Reduce the number of facets in a shape model. The argument should be between 0 and 1. " - + "For example, if a model has 100 facets and the argument to -decimate is 0.90, " - + "there will be approximately 10 facets after the decimation.") - .build()); - options.addOption( - Option.builder("input") - .required() - .hasArg() - .desc( - "Required. Name of shape model to transform. Extension must be icq, fits, llr, obj, pds, plt, ply, sbmt, stl, sum, or vtk. " - + "Alternately transform a single point using three floating point numbers separated " - + "by commas to specify XYZ coordinates, or latitude, longitude in degrees separated by commas. " - + "Transformed point will be written to stdout in the same format as the input string.") - .build()); - options.addOption( - Option.builder("inputFormat") - .hasArg() - .desc( - "Format of input file. If not present format will be inferred from inputFile extension.") - .build()); - options.addOption( - Option.builder("output") - .hasArg() - .desc( - "Required for all but single point input. Name of transformed file. " - + "Extension must be obj, plt, sbmt, stl, sum, or vtk.") - .build()); - options.addOption( - Option.builder("outputFormat") - .hasArg() - .desc( - "Format of output file. If not present format will be inferred from outputFile extension.") - .build()); - options.addOption( - Option.builder("register") - .hasArg() - .desc("Use SVD to transform input file to best align with register file.") - .build()); - options.addOption( - Option.builder("rotate") - .hasArg() - .desc( - "Rotate surface points and spacecraft position. " - + "Specify by an angle (degrees) and a 3 element rotation axis vector (XYZ) " - + "separated by commas.") - .build()); - options.addOption( - Option.builder("rotateToPrincipalAxes") - .desc("Rotate body to align along its principal axes of inertia.") - .build()); - options.addOption( - Option.builder("scale") - .hasArg() - .desc( - "Scale the shape model by . This can either be one value or three " - + "separated by commas. One value scales all three axes uniformly, " - + "three values scale the x, y, and z axes respectively. For example, " - + "-scale 0.5,0.25,1.5 scales the model in the x dimension by 0.5, the " - + "y dimension by 0.25, the z dimension by 1.5.") - .build()); - options.addOption( - Option.builder("translate") - .hasArg() - .desc( - "Translate surface points and spacecraft position. " - + "Specify by three floating point numbers separated by commas.") - .build()); - options.addOption( - Option.builder("translateToCenter") - .desc("Translate body so that its center of mass is at the origin.") - .build()); - options.addOption( - Option.builder("transform") - .hasArg() - .desc( - "Translate and rotate surface points and spacecraft position. " - + "Specify a file containing a 4x4 combined translation/rotation matrix. The top left 3x3 matrix " - + "is the rotation matrix. The top three entries in the right hand column are the translation " - + "vector. The bottom row is always 0 0 0 1.") - .build()); - return options; - } - - public static void main(String[] args) throws Exception { - - TerrasaurTool defaultOBJ = new ShapeFormatConverter(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - NativeLibraryLoader.loadSpiceLibraries(); - NativeLibraryLoader.loadVtkLibraries(); - - String filename = cl.getOptionValue("input"); - COORDTYPE coordType = COORDTYPE.POLYDATA; - vtkPolyData polydata = null; - SumFile sumFile = null; - List sbmtEllipse = null; - - String extension = null; - if (cl.hasOption("inputFormat")) { - try { - extension = - FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()).name().toLowerCase(); - } catch (IllegalArgumentException e) { - logger.warn("Unsupported -inputFormat {}", cl.getOptionValue("inputFormat")); - } - } - if (extension == null) extension = FilenameUtils.getExtension(filename).toLowerCase(); - switch (extension) { - case "icq", "llr", "obj", "pds", "plt", "ply", "stl", "vtk" -> - polydata = PolyDataUtil.loadShapeModel(filename, extension); - case "fits" -> polydata = PolyDataUtil.loadFITShapeModel(filename); - case "sum" -> { - List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); - sumFile = SumFile.fromLines(lines); - } - case "sbmt" -> { - sbmtEllipse = new ArrayList<>(); - vtkPoints points = new vtkPoints(); - polydata = new vtkPolyData(); - polydata.SetPoints(points); - for (String line : FileUtils.readLines(new File(filename), Charset.defaultCharset())) { - SBMTEllipseRecord record = SBMTEllipseRecord.fromString(line); - sbmtEllipse.add(record); - points.InsertNextPoint(record.x(), record.y(), record.z()); - } - } - default -> { - // Single point - String[] params = filename.split(","); - vtkPoints points = new vtkPoints(); - polydata = new vtkPolyData(); - polydata.SetPoints(points); - if (params.length == 2) { - double[] array = - new Vector3D( - Math.toRadians(Double.parseDouble(params[0].trim())), - Math.toRadians(Double.parseDouble(params[1].trim()))) - .toArray(); - points.InsertNextPoint(array); - coordType = COORDTYPE.LATLON; - } else if (params.length == 3) { - double[] array = new double[3]; - for (int i = 0; i < 3; i++) array[i] = Double.parseDouble(params[i].trim()); - points.InsertNextPoint(array); - coordType = COORDTYPE.XYZ; - } else { - logger.error( - "Can't read input shape model {} with format {}", filename, extension.toUpperCase()); - System.exit(0); - } - } + @Override + public String shortDescription() { + return "Transform a shape model to a new coordinate system."; } - if (cl.hasOption("decimate") && polydata != null) { - double reduction = Double.parseDouble(cl.getOptionValue("decimate")); - if (reduction < 0) { - logger.printf(Level.WARN, "Argument to -decimate is %.f! Setting to zero.", reduction); - reduction = 0; - } - if (reduction > 1) { - logger.printf(Level.WARN, "Argument to -decimate is %.f! Setting to one.", reduction); - reduction = 1; - } - PolyDataUtil.decimatePolyData(polydata, reduction); + @Override + public String fullDescription(Options options) { + + String header = ""; + String footer = + "This program will rotate, translate, and/or scale a shape model. It can additionally transform a " + + "single point, a sum file, or an SBMT ellipse file. For a sum file, the SCOBJ vector is " + + "transformed and the cx, cy, cz, and sz vectors are rotated. For SBMT ellipse files, only " + + "center of the ellipse is transformed. The size, orientation, and all other fields in the " + + "file are unchanged."; + return TerrasaurTool.super.fullDescription(options, header, footer); } - if (coordType == COORDTYPE.POLYDATA && !cl.hasOption("output")) { - logger.error(String.format("No output file specified for input file %s", filename)); - System.exit(0); + private enum COORDTYPE { + LATLON, + XYZ, + POLYDATA } - Vector3 centerOfRotation = null; - Matrix33 rotation = null; - Vector3 translation = null; - Vector3 scale = new Vector3(1., 1., 1.); - for (Option option : cl.getOptions()) { - if (option.getOpt().equals("centerOfRotation")) - centerOfRotation = - MathConversions.toVector3( - VectorUtils.stringToVector3D(cl.getOptionValue("centerOfRotation"))); - - if (option.getOpt().equals("rotate")) - rotation = - MathConversions.toMatrix33(RotationUtils.stringToRotation(cl.getOptionValue("rotate"))); - - if (option.getOpt().equals("scale")) { - String scaleString = cl.getOptionValue("scale"); - if (scaleString.contains(",")) { - scale = MathConversions.toVector3(VectorUtils.stringToVector3D(scaleString)); - } else { - scale = scale.scale(Double.parseDouble(scaleString)); - } - } - - if (option.getOpt().equals("translate")) - translation = - MathConversions.toVector3(VectorUtils.stringToVector3D(cl.getOptionValue("translate"))); - - if (option.getOpt().equals("transform")) { - List lines = - FileUtils.readLines(new File(cl.getOptionValue("transform")), Charset.defaultCharset()); - Pair pair = RotationUtils.stringToTransform(lines); - translation = MathConversions.toVector3(pair.getKey()); - rotation = MathConversions.toMatrix33(pair.getValue()); - } + private enum FORMATS { + ICQ, + LLR, + OBJ, + PDS, + PLT, + PLY, + STL, + VTK, + FITS, + SUM, + SBMT } - if (cl.hasOption("rotateToPrincipalAxes")) { - if (polydata != null) { - PolyDataStatistics stats = new PolyDataStatistics(polydata); - if (stats.isClosed()) { - ArrayList axes = stats.getPrincipalAxes(); - // make X primary, Y secondary - rotation = new Matrix33(new Vector3(axes.get(0)), 1, new Vector3(axes.get(1)), 2); - } else { - logger.warn("Shape is not closed, cannot determine principal axes."); - } - } + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("centerOfRotation") + .hasArg() + .desc( + "Subtract this point before applying rotation matrix, add back after. " + + "Specify by three floating point numbers separated by commas. If not present default is (0,0,0).") + .build()); + options.addOption(Option.builder("decimate") + .hasArg() + .desc("Reduce the number of facets in a shape model. The argument should be between 0 and 1. " + + "For example, if a model has 100 facets and the argument to -decimate is 0.90, " + + "there will be approximately 10 facets after the decimation.") + .build()); + options.addOption(Option.builder("input") + .required() + .hasArg() + .desc( + "Required. Name of shape model to transform. Extension must be icq, fits, llr, obj, pds, plt, ply, sbmt, stl, sum, or vtk. " + + "Alternately transform a single point using three floating point numbers separated " + + "by commas to specify XYZ coordinates, or latitude, longitude in degrees separated by commas. " + + "Transformed point will be written to stdout in the same format as the input string.") + .build()); + options.addOption(Option.builder("inputFormat") + .hasArg() + .desc("Format of input file. If not present format will be inferred from inputFile extension.") + .build()); + options.addOption(Option.builder("output") + .hasArg() + .desc("Required for all but single point input. Name of transformed file. " + + "Extension must be obj, plt, sbmt, stl, sum, or vtk.") + .build()); + options.addOption(Option.builder("outputFormat") + .hasArg() + .desc("Format of output file. If not present format will be inferred from outputFile extension.") + .build()); + options.addOption(Option.builder("register") + .hasArg() + .desc("Use SVD to transform input file to best align with register file.") + .build()); + options.addOption(Option.builder("rotate") + .hasArg() + .desc("Rotate surface points and spacecraft position. " + + "Specify by an angle (degrees) and a 3 element rotation axis vector (XYZ) " + + "separated by commas.") + .build()); + options.addOption(Option.builder("rotateToPrincipalAxes") + .desc("Rotate body to align along its principal axes of inertia.") + .build()); + options.addOption(Option.builder("scale") + .hasArg() + .desc("Scale the shape model by . This can either be one value or three " + + "separated by commas. One value scales all three axes uniformly, " + + "three values scale the x, y, and z axes respectively. For example, " + + "-scale 0.5,0.25,1.5 scales the model in the x dimension by 0.5, the " + + "y dimension by 0.25, the z dimension by 1.5.") + .build()); + options.addOption(Option.builder("translate") + .hasArg() + .desc("Translate surface points and spacecraft position. " + + "Specify by three floating point numbers separated by commas.") + .build()); + options.addOption(Option.builder("translateToCenter") + .desc("Translate body so that its center of mass is at the origin.") + .build()); + options.addOption(Option.builder("transform") + .hasArg() + .desc("Translate and rotate surface points and spacecraft position. " + + "Specify a file containing a 4x4 combined translation/rotation matrix. The top left 3x3 matrix " + + "is the rotation matrix. The top three entries in the right hand column are the translation " + + "vector. The bottom row is always 0 0 0 1.") + .build()); + return options; } - if (cl.hasOption("register")) { - String register = cl.getOptionValue("register"); - vtkPolyData registeredPolydata = null; - extension = FilenameUtils.getExtension(register).toLowerCase(); - if (extension.equals("llr") - || extension.equals("obj") - || extension.equals("pds") - || extension.equals("plt") - || extension.equals("ply") - || extension.equals("stl") - || extension.equals("vtk")) { - registeredPolydata = PolyDataUtil.loadShapeModelAndComputeNormals(register); - } else { - logger.error(String.format("Can't read input shape model for registration: %s", register)); - System.exit(0); - } + public static void main(String[] args) throws Exception { - if (registeredPolydata != null) { - Vector3D centerA = PolyDataUtil.computePolyDataCentroid(polydata); - Vector3D centerB = PolyDataUtil.computePolyDataCentroid(registeredPolydata); + TerrasaurTool defaultOBJ = new ShapeFormatConverter(); - vtkPoints points = polydata.GetPoints(); - double[][] pointsA = new double[(int) points.GetNumberOfPoints()][3]; - for (int i = 0; i < points.GetNumberOfPoints(); i++) - pointsA[i] = new Vector3D(points.GetPoint(i)).subtract(centerA).toArray(); - points = registeredPolydata.GetPoints(); + Options options = defineOptions(); - if (points.GetNumberOfPoints() != polydata.GetPoints().GetNumberOfPoints()) { - logger.error("registered polydata does not have the same number of points as input."); - System.exit(0); - } + CommandLine cl = defaultOBJ.parseArgs(args, options); - double[][] pointsB = new double[(int) points.GetNumberOfPoints()][3]; - for (int i = 0; i < points.GetNumberOfPoints(); i++) - pointsB[i] = new Vector3D(points.GetPoint(i)).subtract(centerB).toArray(); + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - double[][] H = new double[3][3]; - for (int ii = 0; ii < points.GetNumberOfPoints(); ii++) { - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - H[i][j] += pointsA[ii][i] * pointsB[ii][j]; + NativeLibraryLoader.loadSpiceLibraries(); + NativeLibraryLoader.loadVtkLibraries(); + + String filename = cl.getOptionValue("input"); + COORDTYPE coordType = COORDTYPE.POLYDATA; + vtkPolyData polydata = null; + SumFile sumFile = null; + List sbmtEllipse = null; + + String extension = null; + if (cl.hasOption("inputFormat")) { + try { + extension = FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase()) + .name() + .toLowerCase(); + } catch (IllegalArgumentException e) { + logger.warn("Unsupported -inputFormat {}", cl.getOptionValue("inputFormat")); + } + } + if (extension == null) extension = FilenameUtils.getExtension(filename).toLowerCase(); + switch (extension) { + case "icq", "llr", "obj", "pds", "plt", "ply", "stl", "vtk" -> + polydata = PolyDataUtil.loadShapeModel(filename, extension); + case "fits" -> polydata = PolyDataUtil.loadFITShapeModel(filename); + case "sum" -> { + List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); + sumFile = SumFile.fromLines(lines); + } + case "sbmt" -> { + sbmtEllipse = new ArrayList<>(); + vtkPoints points = new vtkPoints(); + polydata = new vtkPolyData(); + polydata.SetPoints(points); + for (String line : FileUtils.readLines(new File(filename), Charset.defaultCharset())) { + SBMTEllipseRecord record = SBMTEllipseRecord.fromString(line); + sbmtEllipse.add(record); + points.InsertNextPoint(record.x(), record.y(), record.z()); + } + } + default -> { + // Single point + String[] params = filename.split(","); + vtkPoints points = new vtkPoints(); + polydata = new vtkPolyData(); + polydata.SetPoints(points); + if (params.length == 2) { + double[] array = new Vector3D( + Math.toRadians(Double.parseDouble(params[0].trim())), + Math.toRadians(Double.parseDouble(params[1].trim()))) + .toArray(); + points.InsertNextPoint(array); + coordType = COORDTYPE.LATLON; + } else if (params.length == 3) { + double[] array = new double[3]; + for (int i = 0; i < 3; i++) array[i] = Double.parseDouble(params[i].trim()); + points.InsertNextPoint(array); + coordType = COORDTYPE.XYZ; + } else { + logger.error("Can't read input shape model {} with format {}", filename, extension.toUpperCase()); + System.exit(0); + } } - } } - RealMatrix pointMatrix = new Array2DRowRealMatrix(H); - - SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); - RealMatrix uT = svd.getUT(); - RealMatrix v = svd.getV(); - RealMatrix R = v.multiply(uT); - - if (new LUDecomposition(R).getDeterminant() < 0) { - for (int i = 0; i < 3; i++) { - R.multiplyEntry(i, 2, -1); - } + if (cl.hasOption("decimate") && polydata != null) { + double reduction = Double.parseDouble(cl.getOptionValue("decimate")); + if (reduction < 0) { + logger.printf(Level.WARN, "Argument to -decimate is %.f! Setting to zero.", reduction); + reduction = 0; + } + if (reduction > 1) { + logger.printf(Level.WARN, "Argument to -decimate is %.f! Setting to one.", reduction); + reduction = 1; + } + PolyDataUtil.decimatePolyData(polydata, reduction); } - rotation = new Matrix33(R.getData()); - translation = MathConversions.toVector3(centerB); - translation = translation.sub(rotation.mxv(MathConversions.toVector3(centerA))); - } - } - if (sumFile != null) { - if (rotation != null && translation != null) - sumFile.transform( - MathConversions.toVector3D(translation), MathConversions.toRotation(rotation)); - } else { - - Vector3 center; - if (polydata.GetNumberOfPoints() > 1) { - PolyDataStatistics stats = new PolyDataStatistics(polydata); - center = new Vector3(stats.getCentroid()); - } else { - center = new Vector3(polydata.GetPoint(0)); - } - if (cl.hasOption("translateToCenter")) translation = center.negate(); - - double[] values = new double[3]; - for (int j = 0; j < 3; j++) values[j] = center.getElt(j) * scale.getElt(j); - Vector3 scaledCenter = new Vector3(values); - - vtkPoints points = polydata.GetPoints(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - Vector3 thisPoint = new Vector3(points.GetPoint(i)); - thisPoint = thisPoint.sub(center); - for (int j = 0; j < 3; j++) values[j] = thisPoint.getElt(j) * scale.getElt(j); - thisPoint = new Vector3(values); - thisPoint = thisPoint.add(scaledCenter); - - if (rotation != null) { - if (centerOfRotation == null) centerOfRotation = new Vector3(); - /*- - else { - System.out.printf("Center of rotation:\n%s\n", centerOfRotation.toString()); - System.out.printf("-centerOfRotation %f,%f,%f\n", centerOfRotation.getElt(0), - centerOfRotation.getElt(1), centerOfRotation.getElt(2)); - } - */ - thisPoint = rotation.mxv(thisPoint.sub(centerOfRotation)).add(centerOfRotation); - } - if (translation != null) thisPoint = thisPoint.add(translation); - points.SetPoint(i, thisPoint.toArray()); - } - } - - /*- - if (rotation != null) { - AxisAndAngle aaa = new AxisAndAngle(rotation); - System.out.printf("Rotation:\n%s\n", rotation.toString()); - System.out.printf("-rotate %.5e,%.5e,%.5e,%.5e\n", Math.toDegrees(aaa.getAngle()), - aaa.getAxis().getElt(0), aaa.getAxis().getElt(1), aaa.getAxis().getElt(2)); - } - - if (translation != null) { - System.out.printf("Translation:\n%s\n", translation.toString()); - System.out.printf("-translate %.5e,%.5e,%.5e\n", translation.getElt(0), translation.getElt(1), - translation.getElt(2)); - } - */ - - if (coordType == COORDTYPE.LATLON) { - double[] pt = new double[3]; - polydata.GetPoint(0, pt); - Vector3D point = new Vector3D(pt); - double lon = Math.toDegrees(point.getAlpha()); - if (lon < 0) lon += 360; - System.out.printf("%.16f,%.16f\n", Math.toDegrees(point.getDelta()), lon); - } else if (coordType == COORDTYPE.XYZ) { - double[] pt = new double[3]; - polydata.GetPoint(0, pt); - System.out.printf("%.16f,%.16f,%.16f\n", pt[0], pt[1], pt[2]); - } else { - filename = cl.getOptionValue("output"); - extension = null; - if (cl.hasOption("outputFormat")) { - try { - extension = - FORMATS.valueOf(cl.getOptionValue("outputFormat").toUpperCase()).name().toLowerCase(); - } catch (IllegalArgumentException e) { - logger.warn("Unsupported -outputFormat {}", cl.getOptionValue("outputFormat")); - } - } - if (extension == null) extension = FilenameUtils.getExtension(filename).toLowerCase(); - - switch (extension) { - case "vtk" -> PolyDataUtil.saveShapeModelAsVTK(polydata, filename); - case "obj" -> PolyDataUtil.saveShapeModelAsOBJ(polydata, filename); - case "plt" -> PolyDataUtil.saveShapeModelAsPLT(polydata, filename); - case "stl" -> PolyDataUtil.saveShapeModelAsSTL(polydata, filename); - case "sum" -> { - try (PrintWriter pw = new PrintWriter(filename)) { - pw.print(sumFile.toString()); - } - } - case "sbmt" -> { - if (sbmtEllipse == null) { - logger.error("No input SBMT ellipse specified!"); + if (coordType == COORDTYPE.POLYDATA && !cl.hasOption("output")) { + logger.error(String.format("No output file specified for input file %s", filename)); System.exit(0); - } - double[] pt = new double[3]; - List transformedRecords = new ArrayList<>(); - for (SBMTEllipseRecord record : sbmtEllipse) { + } + + Vector3 centerOfRotation = null; + Matrix33 rotation = null; + Vector3 translation = null; + Vector3 scale = new Vector3(1., 1., 1.); + for (Option option : cl.getOptions()) { + if (option.getOpt().equals("centerOfRotation")) + centerOfRotation = + MathConversions.toVector3(VectorUtils.stringToVector3D(cl.getOptionValue("centerOfRotation"))); + + if (option.getOpt().equals("rotate")) + rotation = MathConversions.toMatrix33(RotationUtils.stringToRotation(cl.getOptionValue("rotate"))); + + if (option.getOpt().equals("scale")) { + String scaleString = cl.getOptionValue("scale"); + if (scaleString.contains(",")) { + scale = MathConversions.toVector3(VectorUtils.stringToVector3D(scaleString)); + } else { + scale = scale.scale(Double.parseDouble(scaleString)); + } + } + + if (option.getOpt().equals("translate")) + translation = MathConversions.toVector3(VectorUtils.stringToVector3D(cl.getOptionValue("translate"))); + + if (option.getOpt().equals("transform")) { + List lines = + FileUtils.readLines(new File(cl.getOptionValue("transform")), Charset.defaultCharset()); + Pair pair = RotationUtils.stringToTransform(lines); + translation = MathConversions.toVector3(pair.getKey()); + rotation = MathConversions.toMatrix33(pair.getValue()); + } + } + + if (cl.hasOption("rotateToPrincipalAxes")) { + if (polydata != null) { + PolyDataStatistics stats = new PolyDataStatistics(polydata); + if (stats.isClosed()) { + ArrayList axes = stats.getPrincipalAxes(); + // make X primary, Y secondary + rotation = new Matrix33(new Vector3(axes.get(0)), 1, new Vector3(axes.get(1)), 2); + } else { + logger.warn("Shape is not closed, cannot determine principal axes."); + } + } + } + + if (cl.hasOption("register")) { + String register = cl.getOptionValue("register"); + vtkPolyData registeredPolydata = null; + extension = FilenameUtils.getExtension(register).toLowerCase(); + if (extension.equals("llr") + || extension.equals("obj") + || extension.equals("pds") + || extension.equals("plt") + || extension.equals("ply") + || extension.equals("stl") + || extension.equals("vtk")) { + registeredPolydata = PolyDataUtil.loadShapeModelAndComputeNormals(register); + } else { + logger.error(String.format("Can't read input shape model for registration: %s", register)); + System.exit(0); + } + + if (registeredPolydata != null) { + Vector3D centerA = PolyDataUtil.computePolyDataCentroid(polydata); + Vector3D centerB = PolyDataUtil.computePolyDataCentroid(registeredPolydata); + + vtkPoints points = polydata.GetPoints(); + double[][] pointsA = new double[(int) points.GetNumberOfPoints()][3]; + for (int i = 0; i < points.GetNumberOfPoints(); i++) + pointsA[i] = + new Vector3D(points.GetPoint(i)).subtract(centerA).toArray(); + points = registeredPolydata.GetPoints(); + + if (points.GetNumberOfPoints() != polydata.GetPoints().GetNumberOfPoints()) { + logger.error("registered polydata does not have the same number of points as input."); + System.exit(0); + } + + double[][] pointsB = new double[(int) points.GetNumberOfPoints()][3]; + for (int i = 0; i < points.GetNumberOfPoints(); i++) + pointsB[i] = + new Vector3D(points.GetPoint(i)).subtract(centerB).toArray(); + + double[][] H = new double[3][3]; + for (int ii = 0; ii < points.GetNumberOfPoints(); ii++) { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + H[i][j] += pointsA[ii][i] * pointsB[ii][j]; + } + } + } + + RealMatrix pointMatrix = new Array2DRowRealMatrix(H); + + SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); + RealMatrix uT = svd.getUT(); + RealMatrix v = svd.getV(); + RealMatrix R = v.multiply(uT); + + if (new LUDecomposition(R).getDeterminant() < 0) { + for (int i = 0; i < 3; i++) { + R.multiplyEntry(i, 2, -1); + } + } + rotation = new Matrix33(R.getData()); + translation = MathConversions.toVector3(centerB); + translation = translation.sub(rotation.mxv(MathConversions.toVector3(centerA))); + } + } + + if (sumFile != null) { + if (rotation != null && translation != null) + sumFile.transform(MathConversions.toVector3D(translation), MathConversions.toRotation(rotation)); + } else { + + Vector3 center; + if (polydata.GetNumberOfPoints() > 1) { + PolyDataStatistics stats = new PolyDataStatistics(polydata); + center = new Vector3(stats.getCentroid()); + } else { + center = new Vector3(polydata.GetPoint(0)); + } + if (cl.hasOption("translateToCenter")) translation = center.negate(); + + double[] values = new double[3]; + for (int j = 0; j < 3; j++) values[j] = center.getElt(j) * scale.getElt(j); + Vector3 scaledCenter = new Vector3(values); + + vtkPoints points = polydata.GetPoints(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + Vector3 thisPoint = new Vector3(points.GetPoint(i)); + thisPoint = thisPoint.sub(center); + for (int j = 0; j < 3; j++) values[j] = thisPoint.getElt(j) * scale.getElt(j); + thisPoint = new Vector3(values); + thisPoint = thisPoint.add(scaledCenter); + + if (rotation != null) { + if (centerOfRotation == null) centerOfRotation = new Vector3(); + /*- + else { + System.out.printf("Center of rotation:\n%s\n", centerOfRotation.toString()); + System.out.printf("-centerOfRotation %f,%f,%f\n", centerOfRotation.getElt(0), + centerOfRotation.getElt(1), centerOfRotation.getElt(2)); + } + */ + thisPoint = rotation.mxv(thisPoint.sub(centerOfRotation)).add(centerOfRotation); + } + if (translation != null) thisPoint = thisPoint.add(translation); + points.SetPoint(i, thisPoint.toArray()); + } + } + + /*- + if (rotation != null) { + AxisAndAngle aaa = new AxisAndAngle(rotation); + System.out.printf("Rotation:\n%s\n", rotation.toString()); + System.out.printf("-rotate %.5e,%.5e,%.5e,%.5e\n", Math.toDegrees(aaa.getAngle()), + aaa.getAxis().getElt(0), aaa.getAxis().getElt(1), aaa.getAxis().getElt(2)); + } + + if (translation != null) { + System.out.printf("Translation:\n%s\n", translation.toString()); + System.out.printf("-translate %.5e,%.5e,%.5e\n", translation.getElt(0), translation.getElt(1), + translation.getElt(2)); + } + */ + + if (coordType == COORDTYPE.LATLON) { + double[] pt = new double[3]; polydata.GetPoint(0, pt); Vector3D point = new Vector3D(pt); double lon = Math.toDegrees(point.getAlpha()); if (lon < 0) lon += 360; - Builder builder = ImmutableSBMTEllipseRecord.builder().from(record); - builder.x(point.getX()); - builder.y(point.getY()); - builder.z(point.getZ()); - builder.lat(Math.toDegrees(point.getDelta())); - builder.lon(lon); - builder.radius(point.getNorm()); + System.out.printf("%.16f,%.16f\n", Math.toDegrees(point.getDelta()), lon); + } else if (coordType == COORDTYPE.XYZ) { + double[] pt = new double[3]; + polydata.GetPoint(0, pt); + System.out.printf("%.16f,%.16f,%.16f\n", pt[0], pt[1], pt[2]); + } else { + filename = cl.getOptionValue("output"); + extension = null; + if (cl.hasOption("outputFormat")) { + try { + extension = FORMATS.valueOf( + cl.getOptionValue("outputFormat").toUpperCase()) + .name() + .toLowerCase(); + } catch (IllegalArgumentException e) { + logger.warn("Unsupported -outputFormat {}", cl.getOptionValue("outputFormat")); + } + } + if (extension == null) + extension = FilenameUtils.getExtension(filename).toLowerCase(); - transformedRecords.add(builder.build()); - } - try (PrintWriter pw = new PrintWriter(filename)) { - for (SBMTEllipseRecord record : transformedRecords) pw.println(record.toString()); - } + switch (extension) { + case "vtk" -> PolyDataUtil.saveShapeModelAsVTK(polydata, filename); + case "obj" -> PolyDataUtil.saveShapeModelAsOBJ(polydata, filename); + case "plt" -> PolyDataUtil.saveShapeModelAsPLT(polydata, filename); + case "stl" -> PolyDataUtil.saveShapeModelAsSTL(polydata, filename); + case "sum" -> { + try (PrintWriter pw = new PrintWriter(filename)) { + pw.print(sumFile.toString()); + } + } + case "sbmt" -> { + if (sbmtEllipse == null) { + logger.error("No input SBMT ellipse specified!"); + System.exit(0); + } + double[] pt = new double[3]; + List transformedRecords = new ArrayList<>(); + for (SBMTEllipseRecord record : sbmtEllipse) { + polydata.GetPoint(0, pt); + Vector3D point = new Vector3D(pt); + double lon = Math.toDegrees(point.getAlpha()); + if (lon < 0) lon += 360; + Builder builder = ImmutableSBMTEllipseRecord.builder().from(record); + builder.x(point.getX()); + builder.y(point.getY()); + builder.z(point.getZ()); + builder.lat(Math.toDegrees(point.getDelta())); + builder.lon(lon); + builder.radius(point.getNorm()); + + transformedRecords.add(builder.build()); + } + try (PrintWriter pw = new PrintWriter(filename)) { + for (SBMTEllipseRecord record : transformedRecords) pw.println(record.toString()); + } + } + default -> { + logger.error("Can't write output shape model {} with format {}", filename, extension.toUpperCase()); + System.exit(0); + } + } + logger.info("Wrote {}", filename); } - default -> { - logger.error( - "Can't write output shape model {} with format {}", - filename, - extension.toUpperCase()); - System.exit(0); - } - } - logger.info("Wrote {}", filename); } - } } diff --git a/src/main/java/terrasaur/apps/SumFilesFromFlyby.java b/src/main/java/terrasaur/apps/SumFilesFromFlyby.java index 4cd729e..d1c67ba 100644 --- a/src/main/java/terrasaur/apps/SumFilesFromFlyby.java +++ b/src/main/java/terrasaur/apps/SumFilesFromFlyby.java @@ -57,19 +57,19 @@ import terrasaur.utils.spice.SpiceBundle; public class SumFilesFromFlyby implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Create sumfiles for a simplified flyby."; - } + @Override + public String shortDescription() { + return "Create sumfiles for a simplified flyby."; + } - @Override - public String fullDescription(Options options) { + @Override + public String fullDescription(Options options) { - String header = ""; - String footer = - """ + String header = ""; + String footer = + """ This tool creates sumfiles at points along a straight line trajectory past a body to be imaged. @@ -82,402 +82,366 @@ public class SumFilesFromFlyby implements TerrasaurTool { Given these assumptions, the trajectory can be specified using closest approach distance and phase along with speed. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private SumFile sumfile; - private Function scPosFunc; - private Function scVelFunc; - - private SumFilesFromFlyby() {} - - public SumFilesFromFlyby(SumFile sumfile, double distance, double phase, double speed) { - this.sumfile = sumfile; - - // given phase angle p, closest approach point is (cos p, sin p) - Vector3D closestApproach = - new Vector3D(FastMath.cos(phase), FastMath.sin(phase), 0.).scalarMultiply(distance); - Vector3D velocity = - new Vector3D(-FastMath.sin(phase), FastMath.cos(phase), 0.).scalarMultiply(speed); - - /*- - * Assumptions: - * - * Sun lies along the X axis - * flyby is in the equatorial (XY) plane - * - */ - scPosFunc = t -> closestApproach.add(velocity.scalarMultiply(t)); - - scVelFunc = t -> velocity; - } - - public SumFile getSumFile(double t) { - - TimeConversion tc = TimeConversion.createUsingInternalConstants(); - - Builder builder = ImmutableSumFile.builder().from(sumfile); - double imageTime = t + tc.utcStringToTDB(sumfile.utcString()); - builder.picnm(String.format("%s%d", sumfile.picnm(), (int) Math.round(imageTime))); - builder.utcString(tc.format("C").apply(imageTime)); - - Vector3D scPos = scPosFunc.apply(t); - - builder.scobj(scPos.negate()); - - Rotation bodyFixedToCamera = RotationUtils.KprimaryJsecondary(scPos.negate(), Vector3D.MINUS_K); - builder.cx(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_I)); - builder.cy(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_J)); - builder.cz(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_K)); - - builder.sz(Vector3D.PLUS_I.scalarMultiply(1e8)); - - SumFile s = builder.build(); - - logger.info( - "{}: S/C position {}, phase {}", - s.utcString(), - s.scobj().negate(), - Math.toDegrees(Vector3D.angle(s.scobj().negate(), s.sz()))); - - return s; - } - - private String writeMSOPCKFiles( - String basename, IntervalSet intervals, int frameID, SpiceBundle bundle) - throws SpiceException { - - File commentFile = new File(basename + "_msopck-comments.txt"); - if (commentFile.exists()) commentFile.delete(); - String setupFile = basename + "_msopck.setup"; - String inputFile = basename + "_msopck.inp"; - - try (PrintWriter pw = new PrintWriter(commentFile)) { - - String allComments = ""; - for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); + return TerrasaurTool.super.fullDescription(options, header, footer); } - File fk = bundle.findKernel(String.format(".*%s\\.tf", basename)); - File lsk = bundle.findKernel(".*naif[0-9]{4}\\.tls"); + private SumFile sumfile; + private Function scPosFunc; + private Function scVelFunc; - Map map = new TreeMap<>(); - map.put("LSK_FILE_NAME", "'" + lsk + "'"); - map.put("MAKE_FAKE_SCLK", String.format("'%s.tsc'", basename)); - map.put("CK_TYPE", "3"); - map.put("COMMENTS_FILE_NAME", String.format("'%s'", commentFile.getPath())); - map.put("INSTRUMENT_ID", Integer.toString(frameID)); - map.put("REFERENCE_FRAME_NAME", "'J2000'"); - map.put("FRAMES_FILE_NAME", "'" + fk.getPath() + "'"); - map.put("ANGULAR_RATE_PRESENT", "'MAKE UP/NO AVERAGING'"); - map.put("INPUT_TIME_TYPE", "'UTC'"); - map.put("INPUT_DATA_TYPE", "'SPICE QUATERNIONS'"); - map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); + private SumFilesFromFlyby() {} - try (PrintWriter pw = new PrintWriter(setupFile)) { - pw.println("\\begindata"); - for (String key : map.keySet()) { - pw.printf("%s = %s\n", key, map.get(key)); - } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); + public SumFilesFromFlyby(SumFile sumfile, double distance, double phase, double speed) { + this.sumfile = sumfile; + + // given phase angle p, closest approach point is (cos p, sin p) + Vector3D closestApproach = new Vector3D(FastMath.cos(phase), FastMath.sin(phase), 0.).scalarMultiply(distance); + Vector3D velocity = new Vector3D(-FastMath.sin(phase), FastMath.cos(phase), 0.).scalarMultiply(speed); + + /*- + * Assumptions: + * + * Sun lies along the X axis + * flyby is in the equatorial (XY) plane + * + */ + scPosFunc = t -> closestApproach.add(velocity.scalarMultiply(t)); + + scVelFunc = t -> velocity; } - NavigableMap attitudeMap = new TreeMap<>(); + public SumFile getSumFile(double t) { - double t0 = bundle.getTimeConversion().utcStringToTDB(sumfile.utcString()); + TimeConversion tc = TimeConversion.createUsingInternalConstants(); - for (UnwritableInterval interval : intervals) { - for (double t = interval.getBegin(); t < interval.getEnd(); t += interval.getLength() / 100) { - - double imageTime = t + t0; + Builder builder = ImmutableSumFile.builder().from(sumfile); + double imageTime = t + tc.utcStringToTDB(sumfile.utcString()); + builder.picnm(String.format("%s%d", sumfile.picnm(), (int) Math.round(imageTime))); + builder.utcString(tc.format("C").apply(imageTime)); Vector3D scPos = scPosFunc.apply(t); - SpiceQuaternion q = - new SpiceQuaternion( - MathConversions.toMatrix33( - RotationUtils.KprimaryJsecondary(scPos.negate(), Vector3D.MINUS_K))); - attitudeMap.put(imageTime, q); - } + builder.scobj(scPos.negate()); + + Rotation bodyFixedToCamera = RotationUtils.KprimaryJsecondary(scPos.negate(), Vector3D.MINUS_K); + builder.cx(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_I)); + builder.cy(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_J)); + builder.cz(bodyFixedToCamera.applyInverseTo(Vector3D.PLUS_K)); + + builder.sz(Vector3D.PLUS_I.scalarMultiply(1e8)); + + SumFile s = builder.build(); + + logger.info( + "{}: S/C position {}, phase {}", + s.utcString(), + s.scobj().negate(), + Math.toDegrees(Vector3D.angle(s.scobj().negate(), s.sz()))); + + return s; } - try (PrintWriter pw = new PrintWriter(new FileWriter(inputFile))) { - for (double t : attitudeMap.keySet()) { - SpiceQuaternion q = attitudeMap.get(t); - Vector3 v = q.getVector(); - pw.printf( - "%s %.14e %.14e %.14e %.14e\n", - new TDBTime(t).toUTCString("ISOC", 6), - q.getScalar(), - v.getElt(0), - v.getElt(1), - v.getElt(2)); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } + private String writeMSOPCKFiles(String basename, IntervalSet intervals, int frameID, SpiceBundle bundle) + throws SpiceException { - return String.format("msopck %s %s %s.bc", setupFile, inputFile, basename); - } + File commentFile = new File(basename + "_msopck-comments.txt"); + if (commentFile.exists()) commentFile.delete(); + String setupFile = basename + "_msopck.setup"; + String inputFile = basename + "_msopck.inp"; - /** - * @param basename file basename - * @param intervals time intervals - * @param centerID NAIF id of center body - * @param bundle SPICE bundle - * @return command to run MKSPK - */ - private String writeMKSPKFiles( - String basename, IntervalSet intervals, int centerID, SpiceBundle bundle) { + try (PrintWriter pw = new PrintWriter(commentFile)) { - String commentFile = basename + "_mkspk-comments.txt"; - String setupFile = basename + "_mkspk.setup"; - String inputFile = basename + "_mkspk.inp"; - - try (PrintWriter pw = new PrintWriter(commentFile)) { - - String allComments = ""; - for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - - File lsk = bundle.findKernel(".*naif[0-9]{4}.tls"); - - Map map = new TreeMap<>(); - map.put("INPUT_DATA_TYPE", "'STATES'"); - map.put("OUTPUT_SPK_TYPE", "13"); // hermite polynomial, unevenly spaced in time - map.put("OBJECT_ID", "-999"); - map.put("CENTER_ID", String.format("%d", centerID)); - map.put("COMMENT_FILE", String.format("'%s'", commentFile)); - map.put("REF_FRAME_NAME", "'J2000'"); - map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); - map.put("DATA_ORDER", "'EPOCH X Y Z VX VY VZ'"); - map.put("DATA_DELIMITER", "' '"); - map.put("LEAPSECONDS_FILE", String.format("'%s'", lsk)); - map.put("TIME_WRAPPER", "'# ETSECONDS'"); - map.put("POLYNOM_DEGREE", "7"); - map.put("SEGMENT_ID", "'SPK_STATES_13'"); - map.put("LINES_PER_RECORD", "1"); - try (PrintWriter pw = new PrintWriter(setupFile)) { - pw.println("\\begindata"); - for (String key : map.keySet()) { - pw.printf("%s = %s\n", key, map.get(key)); - } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - - double t0 = bundle.getTimeConversion().utcStringToTDB(sumfile.utcString()); - - try (PrintWriter pw = new PrintWriter(inputFile)) { - for (UnwritableInterval interval : intervals) { - for (double t = interval.getBegin(); - t < interval.getEnd(); - t += interval.getLength() / 100) { - - Vector3D scPos = scPosFunc.apply(t); - Vector3D scVel = scVelFunc.apply(t); - pw.printf( - "%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", - t + t0, - scPos.getX(), - scPos.getY(), - scPos.getZ(), - scVel.getX(), - scVel.getY(), - scVel.getZ()); - } - } - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - return String.format( - "mkspk -setup %s -input %s -output %s.bsp", setupFile, inputFile, basename); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("distance") - .hasArg() - .required() - .desc("Required. Flyby distance from body center in km.") - .build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption( - Option.builder("mk") - .hasArg() - .desc( - "Path to NAIF metakernel. This should contain LSK, FK for the central body, and FK for the spacecraft. This is used by -mkspk and -msopck.") - .build()); - options.addOption( - Option.builder("mkspk") - .hasArg() - .desc( - "If present, create input files for MKSPK. The argument is the NAIF id for the central body (e.g. 10 for the Sun). This option requires -lsk.") - .build()); - options.addOption( - Option.builder("msopck") - .desc("If present, create input files for MSOPCK. This option requires -lsk.") - .build()); - options.addOption( - Option.builder("phase") - .hasArg() - .required() - .desc("Required. Phase angle at closest approach in degrees.") - .build()); - options.addOption( - Option.builder("speed") - .hasArg() - .required() - .desc("Required. Flyby speed at closest approach in km/s.") - .build()); - options.addOption( - Option.builder("template") - .hasArg() - .required() - .desc( - "Required. An existing sumfile to use as a template. Camera parameters are taken from this " - + "file, while camera position and orientation are calculated.") - .build()); - options.addOption( - Option.builder("times") - .hasArgs() - .desc( - "If present, list of times separated by white space. In seconds, relative to closest approach.") - .build()); - return options; - } - - public static void main(String[] args) throws IOException, SpiceException { - TerrasaurTool defaultOBJ = new SumFilesFromFlyby(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - double phase = Double.parseDouble(cl.getOptionValue("phase")); - if (phase < 0 || phase > 180) { - logger.error("Phase angle {} out of range [0, 180]", phase); - System.exit(0); - } - - String sumFileTemplate = cl.getOptionValue("template"); - - String base = FilenameUtils.getBaseName(sumFileTemplate); - String ext = FilenameUtils.getExtension(sumFileTemplate); - SumFilesFromFlyby app = - new SumFilesFromFlyby( - SumFile.fromFile(new File(sumFileTemplate)), - Double.parseDouble(cl.getOptionValue("distance")), - Math.toRadians(phase), - Double.parseDouble(cl.getOptionValue("speed"))); - - NavigableSet times = new TreeSet<>(); - if (cl.hasOption("times")) { - for (String s : cl.getOptionValues("times")) times.add(Double.parseDouble(s)); - } else times.add(0.); - - SpiceBundle bundle = null; - if (cl.hasOption("mk")) { - NativeLibraryLoader.loadSpiceLibraries(); - bundle = - new SpiceBundle.Builder() - .addMetakernels(Collections.singletonList(cl.getOptionValue("mk"))) - .build(); - KernelDatabase.load(cl.getOptionValue("mk")); - } - - TimeConversion tc = - bundle == null ? TimeConversion.createUsingInternalConstants() : bundle.getTimeConversion(); - - for (double t : times) { - SumFile s = app.getSumFile(t); - - try (PrintWriter pw = - new PrintWriter( - String.format("%s_%d.%s", base, (int) tc.utcStringToTDB(s.utcString()), ext))) { - - pw.println(s); - - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); - } - } - - if (cl.hasOption("mkspk")) { - if (bundle == null) { - logger.error("Need -mk to use -mkspk!"); - } else { - IntervalSet.Builder builder = IntervalSet.builder(); - for (Double t : times) { - Double next = times.higher(t); - if (next != null) builder.add(new UnwritableInterval(t, next)); - } - int centerID = Integer.parseInt(cl.getOptionValue("mkspk")); - - String command = app.writeMKSPKFiles(base, builder.build(), centerID, bundle); - logger.info("Command to create SPK:\n{}", command); - } - } - - if (cl.hasOption("msopck")) { - if (bundle == null) { - logger.error("Need -mk to use -msopck!"); - } else { - IntervalSet.Builder builder = IntervalSet.builder(); - for (Double t : times) { - Double next = times.higher(t); - if (next != null) builder.add(new UnwritableInterval(t, next)); + String allComments = ""; + for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); } - final int scID = -999; - final int frameID = scID * 1000; + File fk = bundle.findKernel(String.format(".*%s\\.tf", basename)); + File lsk = bundle.findKernel(".*naif[0-9]{4}\\.tls"); - File spacecraftFK = new File(String.format("%s.tf", base)); - try (PrintWriter pw = new PrintWriter(spacecraftFK)) { - pw.println("\\begindata"); - pw.printf("FRAME_%s_FIXED = %d\n", base, frameID); - pw.printf("FRAME_%d_NAME = '%s_FIXED'\n", frameID, base); - pw.printf("FRAME_%d_CLASS = 3\n", frameID); - pw.printf("FRAME_%d_CENTER = %d\n", frameID, scID); - pw.printf("FRAME_%d_CLASS_ID = %d\n", frameID, frameID); - pw.println("\\begintext"); + Map map = new TreeMap<>(); + map.put("LSK_FILE_NAME", "'" + lsk + "'"); + map.put("MAKE_FAKE_SCLK", String.format("'%s.tsc'", basename)); + map.put("CK_TYPE", "3"); + map.put("COMMENTS_FILE_NAME", String.format("'%s'", commentFile.getPath())); + map.put("INSTRUMENT_ID", Integer.toString(frameID)); + map.put("REFERENCE_FRAME_NAME", "'J2000'"); + map.put("FRAMES_FILE_NAME", "'" + fk.getPath() + "'"); + map.put("ANGULAR_RATE_PRESENT", "'MAKE UP/NO AVERAGING'"); + map.put("INPUT_TIME_TYPE", "'UTC'"); + map.put("INPUT_DATA_TYPE", "'SPICE QUATERNIONS'"); + map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); + + try (PrintWriter pw = new PrintWriter(setupFile)) { + pw.println("\\begindata"); + for (String key : map.keySet()) { + pw.printf("%s = %s\n", key, map.get(key)); + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); } - List kernels = new ArrayList<>(bundle.getKernels()); - kernels.add(spacecraftFK); + NavigableMap attitudeMap = new TreeMap<>(); - bundle = new SpiceBundle.Builder().addKernelList(kernels).build(); + double t0 = bundle.getTimeConversion().utcStringToTDB(sumfile.utcString()); - File spacecraftSCLK = new File(String.format("%s.tsc", base)); - if (spacecraftSCLK.exists()) spacecraftSCLK.delete(); + for (UnwritableInterval interval : intervals) { + for (double t = interval.getBegin(); t < interval.getEnd(); t += interval.getLength() / 100) { - String command = app.writeMSOPCKFiles(base, builder.build(), frameID, bundle); - logger.info("Command to create SPK:\n{}", command); - } + double imageTime = t + t0; + + Vector3D scPos = scPosFunc.apply(t); + SpiceQuaternion q = new SpiceQuaternion( + MathConversions.toMatrix33(RotationUtils.KprimaryJsecondary(scPos.negate(), Vector3D.MINUS_K))); + + attitudeMap.put(imageTime, q); + } + } + + try (PrintWriter pw = new PrintWriter(new FileWriter(inputFile))) { + for (double t : attitudeMap.keySet()) { + SpiceQuaternion q = attitudeMap.get(t); + Vector3 v = q.getVector(); + pw.printf( + "%s %.14e %.14e %.14e %.14e\n", + new TDBTime(t).toUTCString("ISOC", 6), q.getScalar(), v.getElt(0), v.getElt(1), v.getElt(2)); + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + + return String.format("msopck %s %s %s.bc", setupFile, inputFile, basename); + } + + /** + * @param basename file basename + * @param intervals time intervals + * @param centerID NAIF id of center body + * @param bundle SPICE bundle + * @return command to run MKSPK + */ + private String writeMKSPKFiles(String basename, IntervalSet intervals, int centerID, SpiceBundle bundle) { + + String commentFile = basename + "_mkspk-comments.txt"; + String setupFile = basename + "_mkspk.setup"; + String inputFile = basename + "_mkspk.inp"; + + try (PrintWriter pw = new PrintWriter(commentFile)) { + + String allComments = ""; + for (String comment : allComments.split("\\r?\\n")) pw.println(WordUtils.wrap(comment, 80)); + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + + File lsk = bundle.findKernel(".*naif[0-9]{4}.tls"); + + Map map = new TreeMap<>(); + map.put("INPUT_DATA_TYPE", "'STATES'"); + map.put("OUTPUT_SPK_TYPE", "13"); // hermite polynomial, unevenly spaced in time + map.put("OBJECT_ID", "-999"); + map.put("CENTER_ID", String.format("%d", centerID)); + map.put("COMMENT_FILE", String.format("'%s'", commentFile)); + map.put("REF_FRAME_NAME", "'J2000'"); + map.put("PRODUCER_ID", "'Hari.Nair@jhuapl.edu'"); + map.put("DATA_ORDER", "'EPOCH X Y Z VX VY VZ'"); + map.put("DATA_DELIMITER", "' '"); + map.put("LEAPSECONDS_FILE", String.format("'%s'", lsk)); + map.put("TIME_WRAPPER", "'# ETSECONDS'"); + map.put("POLYNOM_DEGREE", "7"); + map.put("SEGMENT_ID", "'SPK_STATES_13'"); + map.put("LINES_PER_RECORD", "1"); + try (PrintWriter pw = new PrintWriter(setupFile)) { + pw.println("\\begindata"); + for (String key : map.keySet()) { + pw.printf("%s = %s\n", key, map.get(key)); + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + + double t0 = bundle.getTimeConversion().utcStringToTDB(sumfile.utcString()); + + try (PrintWriter pw = new PrintWriter(inputFile)) { + for (UnwritableInterval interval : intervals) { + for (double t = interval.getBegin(); t < interval.getEnd(); t += interval.getLength() / 100) { + + Vector3D scPos = scPosFunc.apply(t); + Vector3D scVel = scVelFunc.apply(t); + pw.printf( + "%.16e %.16e %.16e %.16e %.16e %.16e %.16e\n", + t + t0, scPos.getX(), scPos.getY(), scPos.getZ(), scVel.getX(), scVel.getY(), scVel.getZ()); + } + } + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + return String.format("mkspk -setup %s -input %s -output %s.bsp", setupFile, inputFile, basename); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("distance") + .hasArg() + .required() + .desc("Required. Flyby distance from body center in km.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("mk") + .hasArg() + .desc( + "Path to NAIF metakernel. This should contain LSK, FK for the central body, and FK for the spacecraft. This is used by -mkspk and -msopck.") + .build()); + options.addOption(Option.builder("mkspk") + .hasArg() + .desc( + "If present, create input files for MKSPK. The argument is the NAIF id for the central body (e.g. 10 for the Sun). This option requires -lsk.") + .build()); + options.addOption(Option.builder("msopck") + .desc("If present, create input files for MSOPCK. This option requires -lsk.") + .build()); + options.addOption(Option.builder("phase") + .hasArg() + .required() + .desc("Required. Phase angle at closest approach in degrees.") + .build()); + options.addOption(Option.builder("speed") + .hasArg() + .required() + .desc("Required. Flyby speed at closest approach in km/s.") + .build()); + options.addOption(Option.builder("template") + .hasArg() + .required() + .desc("Required. An existing sumfile to use as a template. Camera parameters are taken from this " + + "file, while camera position and orientation are calculated.") + .build()); + options.addOption(Option.builder("times") + .hasArgs() + .desc("If present, list of times separated by white space. In seconds, relative to closest approach.") + .build()); + return options; + } + + public static void main(String[] args) throws IOException, SpiceException { + TerrasaurTool defaultOBJ = new SumFilesFromFlyby(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + double phase = Double.parseDouble(cl.getOptionValue("phase")); + if (phase < 0 || phase > 180) { + logger.error("Phase angle {} out of range [0, 180]", phase); + System.exit(0); + } + + String sumFileTemplate = cl.getOptionValue("template"); + + String base = FilenameUtils.getBaseName(sumFileTemplate); + String ext = FilenameUtils.getExtension(sumFileTemplate); + SumFilesFromFlyby app = new SumFilesFromFlyby( + SumFile.fromFile(new File(sumFileTemplate)), + Double.parseDouble(cl.getOptionValue("distance")), + Math.toRadians(phase), + Double.parseDouble(cl.getOptionValue("speed"))); + + NavigableSet times = new TreeSet<>(); + if (cl.hasOption("times")) { + for (String s : cl.getOptionValues("times")) times.add(Double.parseDouble(s)); + } else times.add(0.); + + SpiceBundle bundle = null; + if (cl.hasOption("mk")) { + NativeLibraryLoader.loadSpiceLibraries(); + bundle = new SpiceBundle.Builder() + .addMetakernels(Collections.singletonList(cl.getOptionValue("mk"))) + .build(); + KernelDatabase.load(cl.getOptionValue("mk")); + } + + TimeConversion tc = bundle == null ? TimeConversion.createUsingInternalConstants() : bundle.getTimeConversion(); + + for (double t : times) { + SumFile s = app.getSumFile(t); + + try (PrintWriter pw = + new PrintWriter(String.format("%s_%d.%s", base, (int) tc.utcStringToTDB(s.utcString()), ext))) { + + pw.println(s); + + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + + if (cl.hasOption("mkspk")) { + if (bundle == null) { + logger.error("Need -mk to use -mkspk!"); + } else { + IntervalSet.Builder builder = IntervalSet.builder(); + for (Double t : times) { + Double next = times.higher(t); + if (next != null) builder.add(new UnwritableInterval(t, next)); + } + int centerID = Integer.parseInt(cl.getOptionValue("mkspk")); + + String command = app.writeMKSPKFiles(base, builder.build(), centerID, bundle); + logger.info("Command to create SPK:\n{}", command); + } + } + + if (cl.hasOption("msopck")) { + if (bundle == null) { + logger.error("Need -mk to use -msopck!"); + } else { + IntervalSet.Builder builder = IntervalSet.builder(); + for (Double t : times) { + Double next = times.higher(t); + if (next != null) builder.add(new UnwritableInterval(t, next)); + } + + final int scID = -999; + final int frameID = scID * 1000; + + File spacecraftFK = new File(String.format("%s.tf", base)); + try (PrintWriter pw = new PrintWriter(spacecraftFK)) { + pw.println("\\begindata"); + pw.printf("FRAME_%s_FIXED = %d\n", base, frameID); + pw.printf("FRAME_%d_NAME = '%s_FIXED'\n", frameID, base); + pw.printf("FRAME_%d_CLASS = 3\n", frameID); + pw.printf("FRAME_%d_CENTER = %d\n", frameID, scID); + pw.printf("FRAME_%d_CLASS_ID = %d\n", frameID, frameID); + pw.println("\\begintext"); + } + + List kernels = new ArrayList<>(bundle.getKernels()); + kernels.add(spacecraftFK); + + bundle = new SpiceBundle.Builder().addKernelList(kernels).build(); + + File spacecraftSCLK = new File(String.format("%s.tsc", base)); + if (spacecraftSCLK.exists()) spacecraftSCLK.delete(); + + String command = app.writeMSOPCKFiles(base, builder.build(), frameID, bundle); + logger.info("Command to create SPK:\n{}", command); + } + } } - } } diff --git a/src/main/java/terrasaur/apps/TileLookup.java b/src/main/java/terrasaur/apps/TileLookup.java index ba114a1..f7618c7 100644 --- a/src/main/java/terrasaur/apps/TileLookup.java +++ b/src/main/java/terrasaur/apps/TileLookup.java @@ -64,298 +64,299 @@ import terrasaur.utils.tessellation.FibonacciSphere; */ public class TileLookup implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Locate tiles on the unit sphere."; - } - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = ""; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - /** - * Given the base database name and a tile number, return the path to that database file. For - * example: - * - *
-   * System.out.println(TileLookup.getDBName("/path/to/database/ola.db", 6));
-   * System.out.println(TileLookup.getDBName("./ola.db", 6));
-   * System.out.println(TileLookup.getDBName("ola.db", 6));
-   *
-   * /path/to/database/ola.6.db
-   * ./ola.6.db
-   * ./ola.6.db
-   * 
- * - * @param dbName basename for database (e.g. /path/to/database/ola.db) - * @param tile tile index (e.g. 6) - * @return path to database file (e.g. /path/to/database/ola.6.db) - */ - public static String getDBName(String dbName, int tile) { - String fullPath = FilenameUtils.getFullPath(dbName); - if (fullPath.trim().isEmpty()) fullPath = "."; - if (!fullPath.endsWith(File.separator)) fullPath += File.separator; - return String.format( - "%s%s.%d.%s", - fullPath, FilenameUtils.getBaseName(dbName), tile, FilenameUtils.getExtension(dbName)); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("nTiles") - .hasArg() - .required() - .desc("Number of points covering the sphere.") - .build()); - options.addOption( - Option.builder("printCoords") - .desc( - "Print a table of points along with their coordinates. Takes precedence over -printStats, -printDistance, and -png.") - .build()); - options.addOption( - Option.builder("printDistance") - .hasArg() - .desc( - "Print a table of points sorted by distance from the input point. " - + "Format of the input point is longitude,latitude in degrees, comma separated without spaces. Takes precedence over -png.") - .build()); - options.addOption( - Option.builder("printStats") - .desc( - "Print statistics on the distances (in degrees) between each point and its nearest neighbor. Takes precedence over -printDistance and -png.") - .build()); - options.addOption( - Option.builder("png") - .hasArg() - .desc( - "Plot points and distance to nearest point in degrees. Argument is the name of the PNG file to write.") - .build()); - return options; - } - - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new TileLookup(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - final int npts = Integer.parseInt(cl.getOptionValue("nTiles")); - FibonacciSphere fs = new FibonacciSphere(npts); - - if (cl.hasOption("printCoords")) { - String header = String.format("%7s, %10s, %9s", "# index", "longitude", "latitude"); - System.out.println(header); - // System.out.printf("%7s, %10s, %9s, %6s\n", "# index", "longitude", "latitude", "mapola"); - for (int i = 0; i < npts; i++) { - LatitudinalVector lv = fs.getTileCenter(i); - double lon = Math.toDegrees(lv.getLongitude()); - if (lon < 0) lon += 360; - double lat = Math.toDegrees(lv.getLatitude()); - System.out.printf("%7d, %10.5f, %9.5f\n", i, lon, lat); - } - System.exit(0); + @Override + public String shortDescription() { + return "Locate tiles on the unit sphere."; } - if (cl.hasOption("printStats")) { - System.out.println( - "Statistics on distances between each point and its nearest neighbor (degrees):"); - System.out.println(fs.getDistanceStats()); - System.exit(0); + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = ""; + return TerrasaurTool.super.fullDescription(options, header, footer); } - if (cl.hasOption("printDistance")) { - String[] parts = cl.getOptionValue("printDistance").split(","); - double lon = Math.toRadians(Double.parseDouble(parts[0].trim())); - double lat = Math.toRadians(Double.parseDouble(parts[1].trim())); - LatitudinalVector lv = new LatitudinalVector(1, lat, lon); - NavigableMap distanceMap = fs.getDistanceMap(lv); - System.out.printf("%11s, %5s, %10s, %9s\n", "# distance", "index", "longitude", "latitude"); - System.out.printf("%11s, %5s, %10s, %9s\n", "# (degrees)", "", "(degrees)", "(degrees)"); - for (Double dist : distanceMap.keySet()) { - int index = distanceMap.get(dist); - lv = fs.getTileCenter(index); - System.out.printf( - "%11.5f, %5d, %10.5f, %9.5f\n", - Math.toDegrees(dist), - index, - Math.toDegrees(lv.getLongitude()), - Math.toDegrees(lv.getLatitude())); - } - System.exit(0); + /** + * Given the base database name and a tile number, return the path to that database file. For + * example: + * + *
+     * System.out.println(TileLookup.getDBName("/path/to/database/ola.db", 6));
+     * System.out.println(TileLookup.getDBName("./ola.db", 6));
+     * System.out.println(TileLookup.getDBName("ola.db", 6));
+     *
+     * /path/to/database/ola.6.db
+     * ./ola.6.db
+     * ./ola.6.db
+     * 
+ * + * @param dbName basename for database (e.g. /path/to/database/ola.db) + * @param tile tile index (e.g. 6) + * @return path to database file (e.g. /path/to/database/ola.6.db) + */ + public static String getDBName(String dbName, int tile) { + String fullPath = FilenameUtils.getFullPath(dbName); + if (fullPath.trim().isEmpty()) fullPath = "."; + if (!fullPath.endsWith(File.separator)) fullPath += File.separator; + return String.format( + "%s%s.%d.%s", fullPath, FilenameUtils.getBaseName(dbName), tile, FilenameUtils.getExtension(dbName)); } - if (cl.hasOption("png")) { - PlotConfig config = ImmutablePlotConfig.builder().width(1000).height(1000).build(); - - String title = String.format("Fibonacci Sphere, n = %d, ", npts); - - Map projections = new LinkedHashMap<>(); - projections.put( - title + "Rectangular Projection", - new ProjectionRectangular(config.width(), config.height() / 2)); - projections.put( - title + "Orthographic Projection (0, 90)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, Math.PI / 2, 0))); - projections.put( - title + "Orthographic Projection (0, 0)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, 0, 0))); - projections.put( - title + "Orthographic Projection (90, 0)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, 0, Math.PI / 2))); - projections.put( - title + "Orthographic Projection (180, 0)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, 0, Math.PI))); - projections.put( - title + "Orthographic Projection (270, 0)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, 0, 3 * Math.PI / 2))); - projections.put( - title + "Orthographic Projection (0, -90)", - new ProjectionOrthographic( - config.width(), config.height(), new LatitudinalVector(1, -Math.PI / 2, 0))); - - final int nColors = 6; - ColorRamp ramp = ColorRamp.createLinear(1, nColors - 1); - List colors = new ArrayList<>(); - colors.add(Color.BLACK); - for (int i = 1; i < nColors; i++) colors.add(ramp.getColor(i)); - colors.add(Color.WHITE); - ramp = ImmutableColorRamp.builder().min(0).max(nColors).colors(colors).build(); - - double radius = fs.getDistanceStats().getMax(); - ramp = ColorRamp.createLinear(0, radius).addLimitColors(); - - ArrayList images = new ArrayList<>(); - for (String t : projections.keySet()) { - config = ImmutablePlotConfig.builder().from(config).title(t).build(); - Projection p = projections.get(t); - - if (p instanceof ProjectionRectangular) - config = ImmutablePlotConfig.builder().from(config).height(500).build(); - else config = ImmutablePlotConfig.builder().from(config).height(1000).build(); - - MapPlot canvas = new MapPlot(config, p); - AxisX xLowerAxis = new AxisX(0, 360, "Longitude (degrees)", "%.0fE"); - AxisY yLeftAxis = new AxisY(-90, 90, "Latitude (degrees)", "%.0f"); - - canvas.drawTitle(); - canvas.setAxes(xLowerAxis, yLeftAxis); - // canvas.drawAxes(); - - BufferedImage image = canvas.getImage(); - for (int i = 0; i < config.width(); i++) { - for (int j = 0; j < config.height(); j++) { - LatitudinalVector lv = p.pixelToSpherical(i, j); - if (lv == null) continue; - double closestDistance = Math.toDegrees(fs.getNearest(lv).getKey()); - // int numPoints = fs.getDistanceMap(lv).subMap(0., Math.toRadians(radius)).size(); - image.setRGB( - config.leftMargin() + i, - config.topMargin() + j, - ramp.getColor(closestDistance).getRGB()); - } - } - - DiscreteDataSet points = new DiscreteDataSet(""); - for (int i = 0; i < fs.getNumTiles(); i++) { - LatitudinalVector lv = fs.getTileCenter(i); - points.add(lv.getLongitude(), lv.getLatitude()); - } - - if (p instanceof ProjectionRectangular) { - canvas.drawColorBar( - ImmutableColorBar.builder() - .rect( - new Rectangle( - canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) - .ramp(ramp) - .numTicks(nColors + 1) - .tickFunction(aDouble -> String.format("%.1f", aDouble)) - .build()); - -// for (int i = 0; i < fs.getNumTiles(); i++) { -// LatitudinalVector lv = fs.getTileCenter(i); -// canvas.drawCircle(lv, radius, Math.toRadians(1), Color.RED); -// } - } - - for (int i = 0; i < fs.getNumTiles(); i++) { - LatitudinalVector lv = fs.getTileCenter(i); - canvas.addAnnotation( - ImmutableAnnotation.builder().text(String.format("%d", i)).build(), - lv.getLongitude(), - lv.getLatitude()); - } - - images.add(canvas.getImage()); - } - - int width = 2400; - int height = 2400; - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); - - Graphics2D g = image.createGraphics(); - g.setRenderingHint( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - int imageWidth = width; - int imageHeight = height / 3; - g.drawImage( - images.getFirst(), - width / 6, - 0, - 5 * width / 6, - imageHeight, - 0, - 0, - images.getFirst().getWidth(), - images.getFirst().getHeight(), - null); - - imageWidth = width / 3; - for (int i = 1; i < 4; i++) { - g.drawImage( - images.get(i), - (i - 1) * imageWidth, - imageHeight, - i * imageWidth, - 2 * imageHeight, - 0, - 0, - images.get(i).getWidth(), - images.get(i).getHeight(), - null); - } - for (int i = 4; i < 7; i++) { - g.drawImage( - images.get(i), - (i - 4) * imageWidth, - 2 * imageHeight, - (i - 3) * imageWidth, - 3 * imageHeight, - 0, - 0, - images.get(i).getWidth(), - images.get(i).getHeight(), - null); - } - g.dispose(); - - PlotCanvas.writeImage(cl.getOptionValue("png"), image); + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("nTiles") + .hasArg() + .required() + .desc("Number of points covering the sphere.") + .build()); + options.addOption(Option.builder("printCoords") + .desc( + "Print a table of points along with their coordinates. Takes precedence over -printStats, -printDistance, and -png.") + .build()); + options.addOption(Option.builder("printDistance") + .hasArg() + .desc( + "Print a table of points sorted by distance from the input point. " + + "Format of the input point is longitude,latitude in degrees, comma separated without spaces. Takes precedence over -png.") + .build()); + options.addOption(Option.builder("printStats") + .desc( + "Print statistics on the distances (in degrees) between each point and its nearest neighbor. Takes precedence over -printDistance and -png.") + .build()); + options.addOption(Option.builder("png") + .hasArg() + .desc( + "Plot points and distance to nearest point in degrees. Argument is the name of the PNG file to write.") + .build()); + return options; + } + + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new TileLookup(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + final int npts = Integer.parseInt(cl.getOptionValue("nTiles")); + FibonacciSphere fs = new FibonacciSphere(npts); + + if (cl.hasOption("printCoords")) { + String header = String.format("%7s, %10s, %9s", "# index", "longitude", "latitude"); + System.out.println(header); + // System.out.printf("%7s, %10s, %9s, %6s\n", "# index", "longitude", "latitude", "mapola"); + for (int i = 0; i < npts; i++) { + LatitudinalVector lv = fs.getTileCenter(i); + double lon = Math.toDegrees(lv.getLongitude()); + if (lon < 0) lon += 360; + double lat = Math.toDegrees(lv.getLatitude()); + System.out.printf("%7d, %10.5f, %9.5f\n", i, lon, lat); + } + System.exit(0); + } + + if (cl.hasOption("printStats")) { + System.out.println("Statistics on distances between each point and its nearest neighbor (degrees):"); + System.out.println(fs.getDistanceStats()); + System.exit(0); + } + + if (cl.hasOption("printDistance")) { + String[] parts = cl.getOptionValue("printDistance").split(","); + double lon = Math.toRadians(Double.parseDouble(parts[0].trim())); + double lat = Math.toRadians(Double.parseDouble(parts[1].trim())); + LatitudinalVector lv = new LatitudinalVector(1, lat, lon); + NavigableMap distanceMap = fs.getDistanceMap(lv); + System.out.printf("%11s, %5s, %10s, %9s\n", "# distance", "index", "longitude", "latitude"); + System.out.printf("%11s, %5s, %10s, %9s\n", "# (degrees)", "", "(degrees)", "(degrees)"); + for (Double dist : distanceMap.keySet()) { + int index = distanceMap.get(dist); + lv = fs.getTileCenter(index); + System.out.printf( + "%11.5f, %5d, %10.5f, %9.5f\n", + Math.toDegrees(dist), + index, + Math.toDegrees(lv.getLongitude()), + Math.toDegrees(lv.getLatitude())); + } + System.exit(0); + } + + if (cl.hasOption("png")) { + PlotConfig config = + ImmutablePlotConfig.builder().width(1000).height(1000).build(); + + String title = String.format("Fibonacci Sphere, n = %d, ", npts); + + Map projections = new LinkedHashMap<>(); + projections.put( + title + "Rectangular Projection", new ProjectionRectangular(config.width(), config.height() / 2)); + projections.put( + title + "Orthographic Projection (0, 90)", + new ProjectionOrthographic( + config.width(), config.height(), new LatitudinalVector(1, Math.PI / 2, 0))); + projections.put( + title + "Orthographic Projection (0, 0)", + new ProjectionOrthographic(config.width(), config.height(), new LatitudinalVector(1, 0, 0))); + projections.put( + title + "Orthographic Projection (90, 0)", + new ProjectionOrthographic( + config.width(), config.height(), new LatitudinalVector(1, 0, Math.PI / 2))); + projections.put( + title + "Orthographic Projection (180, 0)", + new ProjectionOrthographic(config.width(), config.height(), new LatitudinalVector(1, 0, Math.PI))); + projections.put( + title + "Orthographic Projection (270, 0)", + new ProjectionOrthographic( + config.width(), config.height(), new LatitudinalVector(1, 0, 3 * Math.PI / 2))); + projections.put( + title + "Orthographic Projection (0, -90)", + new ProjectionOrthographic( + config.width(), config.height(), new LatitudinalVector(1, -Math.PI / 2, 0))); + + final int nColors = 6; + ColorRamp ramp = ColorRamp.createLinear(1, nColors - 1); + List colors = new ArrayList<>(); + colors.add(Color.BLACK); + for (int i = 1; i < nColors; i++) colors.add(ramp.getColor(i)); + colors.add(Color.WHITE); + ramp = ImmutableColorRamp.builder() + .min(0) + .max(nColors) + .colors(colors) + .build(); + + double radius = fs.getDistanceStats().getMax(); + ramp = ColorRamp.createLinear(0, radius).addLimitColors(); + + ArrayList images = new ArrayList<>(); + for (String t : projections.keySet()) { + config = ImmutablePlotConfig.builder().from(config).title(t).build(); + Projection p = projections.get(t); + + if (p instanceof ProjectionRectangular) + config = ImmutablePlotConfig.builder() + .from(config) + .height(500) + .build(); + else + config = ImmutablePlotConfig.builder() + .from(config) + .height(1000) + .build(); + + MapPlot canvas = new MapPlot(config, p); + AxisX xLowerAxis = new AxisX(0, 360, "Longitude (degrees)", "%.0fE"); + AxisY yLeftAxis = new AxisY(-90, 90, "Latitude (degrees)", "%.0f"); + + canvas.drawTitle(); + canvas.setAxes(xLowerAxis, yLeftAxis); + // canvas.drawAxes(); + + BufferedImage image = canvas.getImage(); + for (int i = 0; i < config.width(); i++) { + for (int j = 0; j < config.height(); j++) { + LatitudinalVector lv = p.pixelToSpherical(i, j); + if (lv == null) continue; + double closestDistance = + Math.toDegrees(fs.getNearest(lv).getKey()); + // int numPoints = fs.getDistanceMap(lv).subMap(0., Math.toRadians(radius)).size(); + image.setRGB( + config.leftMargin() + i, + config.topMargin() + j, + ramp.getColor(closestDistance).getRGB()); + } + } + + DiscreteDataSet points = new DiscreteDataSet(""); + for (int i = 0; i < fs.getNumTiles(); i++) { + LatitudinalVector lv = fs.getTileCenter(i); + points.add(lv.getLongitude(), lv.getLatitude()); + } + + if (p instanceof ProjectionRectangular) { + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) + .ramp(ramp) + .numTicks(nColors + 1) + .tickFunction(aDouble -> String.format("%.1f", aDouble)) + .build()); + + // for (int i = 0; i < fs.getNumTiles(); i++) { + // LatitudinalVector lv = fs.getTileCenter(i); + // canvas.drawCircle(lv, radius, Math.toRadians(1), Color.RED); + // } + } + + for (int i = 0; i < fs.getNumTiles(); i++) { + LatitudinalVector lv = fs.getTileCenter(i); + canvas.addAnnotation( + ImmutableAnnotation.builder() + .text(String.format("%d", i)) + .build(), + lv.getLongitude(), + lv.getLatitude()); + } + + images.add(canvas.getImage()); + } + + int width = 2400; + int height = 2400; + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + + Graphics2D g = image.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + int imageWidth = width; + int imageHeight = height / 3; + g.drawImage( + images.getFirst(), + width / 6, + 0, + 5 * width / 6, + imageHeight, + 0, + 0, + images.getFirst().getWidth(), + images.getFirst().getHeight(), + null); + + imageWidth = width / 3; + for (int i = 1; i < 4; i++) { + g.drawImage( + images.get(i), + (i - 1) * imageWidth, + imageHeight, + i * imageWidth, + 2 * imageHeight, + 0, + 0, + images.get(i).getWidth(), + images.get(i).getHeight(), + null); + } + for (int i = 4; i < 7; i++) { + g.drawImage( + images.get(i), + (i - 4) * imageWidth, + 2 * imageHeight, + (i - 3) * imageWidth, + 3 * imageHeight, + 0, + 0, + images.get(i).getWidth(), + images.get(i).getHeight(), + null); + } + g.dispose(); + + PlotCanvas.writeImage(cl.getOptionValue("png"), image); + } } - } } diff --git a/src/main/java/terrasaur/apps/TransformFrame.java b/src/main/java/terrasaur/apps/TransformFrame.java index f991cc1..e8fd833 100644 --- a/src/main/java/terrasaur/apps/TransformFrame.java +++ b/src/main/java/terrasaur/apps/TransformFrame.java @@ -50,127 +50,140 @@ import terrasaur.utils.NativeLibraryLoader; import terrasaur.utils.SPICEUtil; public class TransformFrame implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - - @Override - public String shortDescription() { - return "Transform coordinates between reference frames."; - } - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = "\nThis program transforms coordinates between reference frames.\n"; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private NavigableMap pointsIn; - private NavigableMap pointsOut; - - public TransformFrame() {} - - public void setPoints(NavigableMap pointsIn) { - this.pointsIn = pointsIn; - } - - public void transformCoordinates(String inFrame, String outFrame) { - try { - ReferenceFrame from = new ReferenceFrame(inFrame); - ReferenceFrame to = new ReferenceFrame(outFrame); - pointsOut = new TreeMap<>(SPICEUtil.tdbComparator); - for (TDBTime t : pointsIn.keySet()) { - Matrix33 transform = from.getPositionTransformation(to, t); - pointsOut.put(t, transform.mxv(pointsIn.get(t))); - } - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage()); - } - } - - public void write(String outFile) { - - try (PrintWriter pw = new PrintWriter(outFile)) { - for (TDBTime t : pointsOut.keySet()) { - Vector3 v = pointsOut.get(t); - pw.printf("%.6f,%.6e,%.6e,%.6e\n", t.getTDBSeconds(), v.getElt(0), v.getElt(1), - v.getElt(2)); - } - } catch (FileNotFoundException | SpiceException e) { - logger.error(e.getLocalizedMessage()); + @Override + public String shortDescription() { + return "Transform coordinates between reference frames."; } - } + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = "\nThis program transforms coordinates between reference frames.\n"; + return TerrasaurTool.super.fullDescription(options, header, footer); + } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("inFile").required().hasArg() - .desc("Required. Text file containing comma separated t, x, y, z values. Time is ET.") - .build()); - options.addOption(Option.builder("inFrame").required().hasArg() - .desc("Required. Name of inFile reference frame.").build()); - options.addOption(Option.builder("outFile").required().hasArg() - .desc("Required. Name of output file. It will be in the same format as inFile.").build()); - options.addOption(Option.builder("outFrame").required().hasArg() - .desc("Required. Name of outFile reference frame.").build()); - options.addOption(Option.builder("spice").required().hasArg().desc( - "Required. Name of SPICE metakernel containing kernels needed to make the frame transformation.") - .build()); - options.addOption(Option.builder("logFile").hasArg() - .desc("If present, save screen output to log file.").build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) - sb.append(String.format("%s ", l.name())); - options.addOption(Option.builder("logLevel").hasArg() - .desc("If present, print messages above selected priority. Valid values are " - + sb.toString().trim() + ". Default is INFO.") - .build()); return options; - } + private NavigableMap pointsIn; + private NavigableMap pointsOut; - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new TransformFrame(); + public TransformFrame() {} - Options options = defineOptions(); + public void setPoints(NavigableMap pointsIn) { + this.pointsIn = pointsIn; + } - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - TransformFrame tf = new TransformFrame(); - NavigableMap map = new TreeMap<>(SPICEUtil.tdbComparator); - try { - File f = new File(cl.getOptionValue("inFile")); - List lines = FileUtils.readLines(f, Charset.defaultCharset()); - for (String line : lines) { - String trim = line.trim(); - if (trim.isEmpty() || trim.startsWith("#")) - continue; - String[] parts = trim.split(","); - double et = Double.parseDouble(parts[0].trim()); - if (et > 0) { - TDBTime t = new TDBTime(et); - Vector3 v = new Vector3(Double.parseDouble(parts[1].trim()), - Double.parseDouble(parts[2].trim()), Double.parseDouble(parts[3].trim())); - map.put(t, v); + public void transformCoordinates(String inFrame, String outFrame) { + try { + ReferenceFrame from = new ReferenceFrame(inFrame); + ReferenceFrame to = new ReferenceFrame(outFrame); + pointsOut = new TreeMap<>(SPICEUtil.tdbComparator); + for (TDBTime t : pointsIn.keySet()) { + Matrix33 transform = from.getPositionTransformation(to, t); + pointsOut.put(t, transform.mxv(pointsIn.get(t))); + } + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage()); } - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage()); } - tf.setPoints(map); + public void write(String outFile) { - NativeLibraryLoader.loadSpiceLibraries(); - try { - KernelDatabase.load(cl.getOptionValue("spice")); - } catch (SpiceErrorException e) { - logger.error(e.getLocalizedMessage()); + try (PrintWriter pw = new PrintWriter(outFile)) { + for (TDBTime t : pointsOut.keySet()) { + Vector3 v = pointsOut.get(t); + pw.printf("%.6f,%.6e,%.6e,%.6e\n", t.getTDBSeconds(), v.getElt(0), v.getElt(1), v.getElt(2)); + } + } catch (FileNotFoundException | SpiceException e) { + logger.error(e.getLocalizedMessage()); + } } - tf.transformCoordinates(cl.getOptionValue("inFrame"), cl.getOptionValue("outFrame")); - tf.write(cl.getOptionValue("outFile")); - } + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("inFile") + .required() + .hasArg() + .desc("Required. Text file containing comma separated t, x, y, z values. Time is ET.") + .build()); + options.addOption(Option.builder("inFrame") + .required() + .hasArg() + .desc("Required. Name of inFile reference frame.") + .build()); + options.addOption(Option.builder("outFile") + .required() + .hasArg() + .desc("Required. Name of output file. It will be in the same format as inFile.") + .build()); + options.addOption(Option.builder("outFrame") + .required() + .hasArg() + .desc("Required. Name of outFile reference frame.") + .build()); + options.addOption(Option.builder("spice") + .required() + .hasArg() + .desc("Required. Name of SPICE metakernel containing kernels needed to make the frame transformation.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + ". Default is INFO.") + .build()); + return options; + } + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new TransformFrame(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + TransformFrame tf = new TransformFrame(); + NavigableMap map = new TreeMap<>(SPICEUtil.tdbComparator); + try { + File f = new File(cl.getOptionValue("inFile")); + List lines = FileUtils.readLines(f, Charset.defaultCharset()); + for (String line : lines) { + String trim = line.trim(); + if (trim.isEmpty() || trim.startsWith("#")) continue; + String[] parts = trim.split(","); + double et = Double.parseDouble(parts[0].trim()); + if (et > 0) { + TDBTime t = new TDBTime(et); + Vector3 v = new Vector3( + Double.parseDouble(parts[1].trim()), + Double.parseDouble(parts[2].trim()), + Double.parseDouble(parts[3].trim())); + map.put(t, v); + } + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage()); + } + + tf.setPoints(map); + + NativeLibraryLoader.loadSpiceLibraries(); + try { + KernelDatabase.load(cl.getOptionValue("spice")); + } catch (SpiceErrorException e) { + logger.error(e.getLocalizedMessage()); + } + + tf.transformCoordinates(cl.getOptionValue("inFrame"), cl.getOptionValue("outFrame")); + tf.write(cl.getOptionValue("outFile")); + } } diff --git a/src/main/java/terrasaur/apps/TranslateTime.java b/src/main/java/terrasaur/apps/TranslateTime.java index f4f7d75..6bf6428 100644 --- a/src/main/java/terrasaur/apps/TranslateTime.java +++ b/src/main/java/terrasaur/apps/TranslateTime.java @@ -43,220 +43,232 @@ import terrasaur.utils.NativeLibraryLoader; /** * Translate time between formats. - * + * * @author nairah1 * */ public class TranslateTime implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Convert between different time systems."; - } - - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = "\nConvert between different time systems.\n"; - return TerrasaurTool.super.fullDescription(options, header, footer); - - } - - private enum Types { - JULIAN, SCLK, TDB, TDBCALENDAR, UTC - } - - private Map sclkMap; - - private TranslateTime(){} - - public TranslateTime(Map sclkMap) { - this.sclkMap = sclkMap; - } - - private TDBTime tdb; - - public String toJulian() throws SpiceErrorException { - return tdb.toString("JULIAND.######"); - } - - private SCLK sclkKernel; - - public SCLK getSCLKKernel() { - return sclkKernel; - } - - public void setSCLKKernel(int sclkID) { - sclkKernel = sclkMap.get(sclkID); - if (sclkKernel == null) { - logger.error("SCLK {} is not loaded!", sclkID); - } - } - - public SCLKTime toSCLK() throws SpiceException { - return new SCLKTime(sclkKernel, tdb); - } - - public TDBTime toTDB() { - return tdb; - } - - public String toUTC() throws SpiceErrorException { - return tdb.toUTCString("ISOC", 3); - } - - public void setJulianDate(double julianDate) throws SpiceErrorException { - tdb = new TDBTime(String.format("%.6f JDUTC", julianDate)); - } - - public void setSCLK(String sclkString) throws SpiceException { - tdb = new TDBTime(new SCLKTime(sclkKernel, sclkString)); - } - - public void setTDB(double tdb) { - this.tdb = new TDBTime(tdb); - } - - public void setTDBCalendarString(String tdbString) throws SpiceErrorException { - tdb = new TDBTime(String.format("%s TDB", tdbString)); - } - - public void setUTC(String utcStr) throws SpiceErrorException { - tdb = new TDBTime(utcStr); - } - - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption(Option.builder("logFile").hasArg() - .desc("If present, save screen output to log file.").build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) - sb.append(String.format("%s ", l.name())); - options.addOption(Option.builder("logLevel").hasArg() - .desc("If present, print messages above selected priority. Valid values are " - + sb.toString().trim() + ". Default is INFO.") - .build()); - options.addOption(Option.builder("sclk").hasArg().desc( - "SPICE id of the sclk to use. Default is to use the first one found in the kernel pool.") - .build()); - options.addOption(Option.builder("spice").required().hasArg() - .desc("Required. SPICE metakernel containing leap second and SCLK.").build()); - options.addOption(Option.builder("gui").desc("Launch a GUI.").build()); - options.addOption(Option.builder("inputDate").hasArgs().desc("Date to translate.").build()); - sb = new StringBuilder(); - for (Types system : Types.values()) { - sb.append(String.format("%s ", system.name())); - } - options.addOption(Option.builder("inputSystem").hasArg().desc( - "Timesystem of inputDate. Valid values are " + sb.toString().trim() + ". Default is UTC.") - .build()); return options; - } - - public static void main(String[] args) throws SpiceException { - TerrasaurTool defaultOBJ = new TranslateTime(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - - // This is to avoid java crashing due to inability to connect to an X display - if (!cl.hasOption("gui")) - System.setProperty("java.awt.headless", "true"); - - NativeLibraryLoader.loadSpiceLibraries(); - - for (String kernel : cl.getOptionValues("spice")) - KernelDatabase.load(kernel); - - LinkedHashMap sclkMap = new LinkedHashMap<>(); - String[] sclk_data_type = KernelPool.getNames("SCLK_DATA_*"); - for (String s : sclk_data_type) { - String[] parts = s.split("_"); - int sclkID = -Integer.parseInt(parts[parts.length - 1]); - sclkMap.put(sclkID, new SCLK(sclkID)); + @Override + public String shortDescription() { + return "Convert between different time systems."; } - SCLK sclk = null; - if (cl.hasOption("sclk")) { - int sclkID = Integer.parseInt(cl.getOptionValue("sclk")); - if (sclkMap.containsKey(sclkID)) - sclk = sclkMap.get(sclkID); - else { - logger.error("Cannot find SCLK {} in kernel pool!", sclkID); - StringBuilder sb = new StringBuilder(); - for (Integer id : sclkMap.keySet()) - sb.append(String.format("%d ", id)); - logger.error("Loaded IDs are {}", sb.toString()); - } - } else { - if (!sclkMap.values().isEmpty()) - // set the SCLK to the first one found - sclk = sclkMap.values().stream().toList().get(0); + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = "\nConvert between different time systems.\n"; + return TerrasaurTool.super.fullDescription(options, header, footer); } - if (sclk == null) { - logger.fatal("Cannot load SCLK"); - System.exit(0); + private enum Types { + JULIAN, + SCLK, + TDB, + TDBCALENDAR, + UTC } - TranslateTime tt = new TranslateTime(sclkMap); + private Map sclkMap; - if (cl.hasOption("gui")) { - TranslateTimeFX.setTranslateTime(tt); - TranslateTimeFX.setSCLKIDs(sclkMap.keySet()); - TranslateTimeFX.main(args); - System.exit(0); - } else { - if (!cl.hasOption("inputDate")) { - logger.fatal("Missing required option -inputDate!"); - System.exit(1); - } - tt.setSCLKKernel(sclk.getIDCode()); + private TranslateTime() {} + + public TranslateTime(Map sclkMap) { + this.sclkMap = sclkMap; } - StringBuilder sb = new StringBuilder(); - for (String s : cl.getOptionValues("inputDate")) - sb.append(String.format("%s ", s)); - String inputDate = sb.toString().trim(); + private TDBTime tdb; - Types type = - cl.hasOption("inputSystem") ? Types.valueOf(cl.getOptionValue("inputSystem").toUpperCase()) - : Types.UTC; - - switch (type) { - case JULIAN: - tt.setJulianDate(Double.parseDouble(inputDate)); - break; - case SCLK: - tt.setSCLK(inputDate); - break; - case TDB: - tt.setTDB(Double.parseDouble(inputDate)); - break; - case TDBCALENDAR: - tt.setTDBCalendarString(inputDate); - break; - case UTC: - tt.setUTC(inputDate); - break; + public String toJulian() throws SpiceErrorException { + return tdb.toString("JULIAND.######"); } - System.out.printf("# input date %s (%s)\n", inputDate, type.name()); - System.out.printf("# UTC, TDB (Calendar), DOY, TDB, Julian Date, SCLK (%d)\n", - sclk.getIDCode()); + private SCLK sclkKernel; - String utcString = tt.toTDB().toUTCString("ISOC", 3); - String tdbString = tt.toTDB().toString("YYYY-MM-DDTHR:MN:SC.### ::TDB"); - String doyString = tt.toTDB().toString("DOY"); + public SCLK getSCLKKernel() { + return sclkKernel; + } - System.out.printf("%s, %s, %s, %.6f, %s, %s\n", utcString, tdbString, doyString, - tt.toTDB().getTDBSeconds(), tt.toJulian(), tt.toSCLK().toString()); + public void setSCLKKernel(int sclkID) { + sclkKernel = sclkMap.get(sclkID); + if (sclkKernel == null) { + logger.error("SCLK {} is not loaded!", sclkID); + } + } - } + public SCLKTime toSCLK() throws SpiceException { + return new SCLKTime(sclkKernel, tdb); + } + + public TDBTime toTDB() { + return tdb; + } + + public String toUTC() throws SpiceErrorException { + return tdb.toUTCString("ISOC", 3); + } + + public void setJulianDate(double julianDate) throws SpiceErrorException { + tdb = new TDBTime(String.format("%.6f JDUTC", julianDate)); + } + + public void setSCLK(String sclkString) throws SpiceException { + tdb = new TDBTime(new SCLKTime(sclkKernel, sclkString)); + } + + public void setTDB(double tdb) { + this.tdb = new TDBTime(tdb); + } + + public void setTDBCalendarString(String tdbString) throws SpiceErrorException { + tdb = new TDBTime(String.format("%s TDB", tdbString)); + } + + public void setUTC(String utcStr) throws SpiceErrorException { + tdb = new TDBTime(utcStr); + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + ". Default is INFO.") + .build()); + options.addOption(Option.builder("sclk") + .hasArg() + .desc("SPICE id of the sclk to use. Default is to use the first one found in the kernel pool.") + .build()); + options.addOption(Option.builder("spice") + .required() + .hasArg() + .desc("Required. SPICE metakernel containing leap second and SCLK.") + .build()); + options.addOption(Option.builder("gui").desc("Launch a GUI.").build()); + options.addOption( + Option.builder("inputDate").hasArgs().desc("Date to translate.").build()); + sb = new StringBuilder(); + for (Types system : Types.values()) { + sb.append(String.format("%s ", system.name())); + } + options.addOption(Option.builder("inputSystem") + .hasArg() + .desc("Timesystem of inputDate. Valid values are " + + sb.toString().trim() + ". Default is UTC.") + .build()); + return options; + } + + public static void main(String[] args) throws SpiceException { + TerrasaurTool defaultOBJ = new TranslateTime(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + + // This is to avoid java crashing due to inability to connect to an X display + if (!cl.hasOption("gui")) System.setProperty("java.awt.headless", "true"); + + NativeLibraryLoader.loadSpiceLibraries(); + + for (String kernel : cl.getOptionValues("spice")) KernelDatabase.load(kernel); + + LinkedHashMap sclkMap = new LinkedHashMap<>(); + String[] sclk_data_type = KernelPool.getNames("SCLK_DATA_*"); + for (String s : sclk_data_type) { + String[] parts = s.split("_"); + int sclkID = -Integer.parseInt(parts[parts.length - 1]); + sclkMap.put(sclkID, new SCLK(sclkID)); + } + + SCLK sclk = null; + if (cl.hasOption("sclk")) { + int sclkID = Integer.parseInt(cl.getOptionValue("sclk")); + if (sclkMap.containsKey(sclkID)) sclk = sclkMap.get(sclkID); + else { + logger.error("Cannot find SCLK {} in kernel pool!", sclkID); + StringBuilder sb = new StringBuilder(); + for (Integer id : sclkMap.keySet()) sb.append(String.format("%d ", id)); + logger.error("Loaded IDs are {}", sb.toString()); + } + } else { + if (!sclkMap.values().isEmpty()) + // set the SCLK to the first one found + sclk = sclkMap.values().stream().toList().get(0); + } + + if (sclk == null) { + logger.fatal("Cannot load SCLK"); + System.exit(0); + } + + TranslateTime tt = new TranslateTime(sclkMap); + + if (cl.hasOption("gui")) { + TranslateTimeFX.setTranslateTime(tt); + TranslateTimeFX.setSCLKIDs(sclkMap.keySet()); + TranslateTimeFX.main(args); + System.exit(0); + } else { + if (!cl.hasOption("inputDate")) { + logger.fatal("Missing required option -inputDate!"); + System.exit(1); + } + tt.setSCLKKernel(sclk.getIDCode()); + } + + StringBuilder sb = new StringBuilder(); + for (String s : cl.getOptionValues("inputDate")) sb.append(String.format("%s ", s)); + String inputDate = sb.toString().trim(); + + Types type = cl.hasOption("inputSystem") + ? Types.valueOf(cl.getOptionValue("inputSystem").toUpperCase()) + : Types.UTC; + + switch (type) { + case JULIAN: + tt.setJulianDate(Double.parseDouble(inputDate)); + break; + case SCLK: + tt.setSCLK(inputDate); + break; + case TDB: + tt.setTDB(Double.parseDouble(inputDate)); + break; + case TDBCALENDAR: + tt.setTDBCalendarString(inputDate); + break; + case UTC: + tt.setUTC(inputDate); + break; + } + + System.out.printf("# input date %s (%s)\n", inputDate, type.name()); + System.out.printf("# UTC, TDB (Calendar), DOY, TDB, Julian Date, SCLK (%d)\n", sclk.getIDCode()); + + String utcString = tt.toTDB().toUTCString("ISOC", 3); + String tdbString = tt.toTDB().toString("YYYY-MM-DDTHR:MN:SC.### ::TDB"); + String doyString = tt.toTDB().toString("DOY"); + + System.out.printf( + "%s, %s, %s, %.6f, %s, %s\n", + utcString, + tdbString, + doyString, + tt.toTDB().getTDBSeconds(), + tt.toJulian(), + tt.toSCLK().toString()); + } } diff --git a/src/main/java/terrasaur/apps/TriAx.java b/src/main/java/terrasaur/apps/TriAx.java index 7523347..0eff761 100644 --- a/src/main/java/terrasaur/apps/TriAx.java +++ b/src/main/java/terrasaur/apps/TriAx.java @@ -39,128 +39,121 @@ import vtk.vtkPolyData; public class TriAx implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private TriAx() {} + 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 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]; - } - } - } + @Override + public String shortDescription() { + return "Generate a triaxial ellipsoid in ICQ format."; } - ICQUtils.writeICQ(q, vec, shapefile); + @Override + public String fullDescription(Options options) { - if (cl.hasOption("saveOBJ")) { + 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); + } - String basename = FilenameUtils.getBaseName(shapefile); - String dirname = FilenameUtils.getFullPath(shapefile); - if (dirname.isEmpty()) dirname = "."; - File obj = new File(dirname, basename + ".obj"); + 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()); - NativeLibraryLoader.loadVtkLibraries(); - try { - vtkPolyData polydata = PolyDataUtil.loadShapeModel(shapefile); - if (polydata == null) { - logger.error("Cannot read {}", shapefile); - System.exit(0); + 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 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]; + } + } + } } - polydata = PolyDataUtil.removeDuplicatePoints(polydata); - polydata = PolyDataUtil.removeUnreferencedPoints(polydata); - polydata = PolyDataUtil.removeZeroAreaFacets(polydata); + ICQUtils.writeICQ(q, vec, shapefile); - PolyDataUtil.saveShapeModelAsOBJ(polydata, obj.getPath()); - } catch (Exception e) { - logger.error(e); - } + 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); + } + } } - } } diff --git a/src/main/java/terrasaur/apps/ValidateNormals.java b/src/main/java/terrasaur/apps/ValidateNormals.java index 6397eee..495589d 100644 --- a/src/main/java/terrasaur/apps/ValidateNormals.java +++ b/src/main/java/terrasaur/apps/ValidateNormals.java @@ -45,248 +45,238 @@ import vtk.vtkOBJReader; import vtk.vtkPolyData; public class ValidateNormals implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private ValidateNormals() {} + private ValidateNormals() {} - @Override - public String shortDescription() { - return "Check facet normal directions for an OBJ shape file."; - } - - @Override - public String fullDescription(Options options) { - - String footer = - "\nThis program checks that the normals of the shape model are not pointing inward.\n"; - return TerrasaurTool.super.fullDescription(options, "", footer); - } - - 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() - .desc( - "If present, center of body in xyz coordinates. " - + "Specify as three floating point values separated by commas. Default is to use the centroid of " - + "the input shape model.") - .build()); - options.addOption( - Option.builder("obj").required().hasArg().desc("Shape model to validate.").build()); - options.addOption( - Option.builder("output") - .hasArg() - .desc("Write out new OBJ file with corrected vertex orders for facets.") - .build()); - options.addOption( - Option.builder("numThreads") - .hasArg() - .desc("Number of threads to run. Default is 1.") - .build()); - return options; - } - - private vtkPolyData polyData; - private ThreadLocal threadLocalsearchTree; - private double[] origin; - - public ValidateNormals(vtkPolyData polyData) { - this.polyData = polyData; - - PolyDataStatistics stats = new PolyDataStatistics(polyData); - origin = stats.getCentroid(); - - threadLocalsearchTree = new ThreadLocal<>(); - } - - public vtkOBBTree getOBBTree() { - vtkOBBTree searchTree = threadLocalsearchTree.get(); - if (searchTree == null) { - searchTree = new vtkOBBTree(); - searchTree.SetDataSet(polyData); - searchTree.SetTolerance(1e-12); - searchTree.BuildLocator(); - threadLocalsearchTree.set(searchTree); - } - return searchTree; - } - - public void setOrigin(double[] origin) { - this.origin = origin; - } - - private class FlippedNormalFinder implements Callable> { - - private static final DateTimeFormatter defaultFormatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z") - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()); - - private final long index0; - private final long index1; - private final boolean fast; - - public FlippedNormalFinder(long index0, long index1, boolean fast) { - this.index0 = index0; - this.index1 = index1; - this.fast = fast; + @Override + public String shortDescription() { + return "Check facet normal directions for an OBJ shape file."; } @Override - public List call() { + public String fullDescription(Options options) { - logger.info("Thread {}: indices {} to {}", Thread.currentThread().threadId(), index0, index1); - vtkIdList idList = new vtkIdList(); - vtkIdList cellIds = new vtkIdList(); - List flippedNormals = new ArrayList<>(); + String footer = "\nThis program checks that the normals of the shape model are not pointing inward.\n"; + return TerrasaurTool.super.fullDescription(options, "", footer); + } - final long startTime = Instant.now().getEpochSecond(); - final long numFacets = index1 - index0; - for (int i = 0; i < numFacets; ++i) { + 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() + .desc("If present, center of body in xyz coordinates. " + + "Specify as three floating point values separated by commas. Default is to use the centroid of " + + "the input shape model.") + .build()); + options.addOption(Option.builder("obj") + .required() + .hasArg() + .desc("Shape model to validate.") + .build()); + options.addOption(Option.builder("output") + .hasArg() + .desc("Write out new OBJ file with corrected vertex orders for facets.") + .build()); + options.addOption(Option.builder("numThreads") + .hasArg() + .desc("Number of threads to run. Default is 1.") + .build()); + return options; + } - if (i > 0 && i % (numFacets / 10) == 0) { - double pctDone = i / (numFacets * .01); - long elapsed = Instant.now().getEpochSecond() - startTime; - long estimatedFinish = (long) (elapsed / (pctDone / 100) + startTime); - String finish = defaultFormatter.format(Instant.ofEpochSecond(estimatedFinish)); - logger.info( - String.format( - "Thread %d: read %d of %d facets. %.0f%% complete, projected finish %s", - Thread.currentThread().threadId(), index0 + i, index1, pctDone, finish)); + private vtkPolyData polyData; + private ThreadLocal threadLocalsearchTree; + private double[] origin; + + public ValidateNormals(vtkPolyData polyData) { + this.polyData = polyData; + + PolyDataStatistics stats = new PolyDataStatistics(polyData); + origin = stats.getCentroid(); + + threadLocalsearchTree = new ThreadLocal<>(); + } + + public vtkOBBTree getOBBTree() { + vtkOBBTree searchTree = threadLocalsearchTree.get(); + if (searchTree == null) { + searchTree = new vtkOBBTree(); + searchTree.SetDataSet(polyData); + searchTree.SetTolerance(1e-12); + searchTree.BuildLocator(); + threadLocalsearchTree.set(searchTree); + } + return searchTree; + } + + public void setOrigin(double[] origin) { + this.origin = origin; + } + + private class FlippedNormalFinder implements Callable> { + + private static final DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z") + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + + private final long index0; + private final long index1; + private final boolean fast; + + public FlippedNormalFinder(long index0, long index1, boolean fast) { + this.index0 = index0; + this.index1 = index1; + this.fast = fast; } - long index = index0 + i; + @Override + public List call() { - CellInfo ci = CellInfo.getCellInfo(polyData, index, idList); - boolean isOpposite = (ci.center().dotProduct(ci.normal()) < 0); + logger.info("Thread {}: indices {} to {}", Thread.currentThread().threadId(), index0, index1); + vtkIdList idList = new vtkIdList(); + vtkIdList cellIds = new vtkIdList(); + List flippedNormals = new ArrayList<>(); - int numCrossings = 0; - 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++; - } + final long startTime = Instant.now().getEpochSecond(); + final long numFacets = index1 - index0; + for (int i = 0; i < numFacets; ++i) { + + if (i > 0 && i % (numFacets / 10) == 0) { + double pctDone = i / (numFacets * .01); + long elapsed = Instant.now().getEpochSecond() - startTime; + long estimatedFinish = (long) (elapsed / (pctDone / 100) + startTime); + String finish = defaultFormatter.format(Instant.ofEpochSecond(estimatedFinish)); + logger.info(String.format( + "Thread %d: read %d of %d facets. %.0f%% complete, projected finish %s", + Thread.currentThread().threadId(), index0 + i, index1, pctDone, finish)); + } + + long index = index0 + i; + + CellInfo ci = CellInfo.getCellInfo(polyData, index, idList); + boolean isOpposite = (ci.center().dotProduct(ci.normal()) < 0); + + int numCrossings = 0; + 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. + boolean shouldBeOpposite = (numCrossings % 2 == 1); + + // XOR operator - true if both conditions are different + if (isOpposite ^ shouldBeOpposite) flippedNormals.add(index); + } + + return flippedNormals; + } + } + + public void flipNormals(Collection facets) { + vtkCellArray cells = new vtkCellArray(); + for (long i = 0; i < polyData.GetNumberOfCells(); ++i) { + vtkIdList idList = new vtkIdList(); + polyData.GetCellPoints(i, idList); + if (facets.contains(i)) { + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + idList.SetId(0, id0); + idList.SetId(1, id2); + idList.SetId(2, id1); + } + cells.InsertNextCell(idList); + } + polyData.SetPolys(cells); + } + + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new ValidateNormals(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) logger.info("{} {}", ml.label, startupMessages.get(ml)); + + NativeLibraryLoader.loadVtkLibraries(); + + // PolyDataUtil's OBJ reader messes with the normals - not reliable for a local obj + vtkOBJReader smallBodyReader = new vtkOBJReader(); + smallBodyReader.SetFileName(cl.getOptionValue("obj")); + smallBodyReader.Update(); + vtkPolyData polyData = new vtkPolyData(); + polyData.ShallowCopy(smallBodyReader.GetOutput()); + + smallBodyReader.Delete(); + + ValidateNormals app = new ValidateNormals(polyData); + + logger.info("Read {} facets from {}", polyData.GetNumberOfCells(), cl.getOptionValue("obj")); + + if (cl.hasOption("origin")) { + String[] parts = cl.getOptionValue("origin").split(","); + double[] origin = new double[3]; + for (int i = 0; i < 3; i++) origin[i] = Double.parseDouble(parts[i]); + app.setOrigin(origin); } - // 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. - boolean shouldBeOpposite = (numCrossings % 2 == 1); + Set flippedNormals = new HashSet<>(); - // XOR operator - true if both conditions are different - if (isOpposite ^ shouldBeOpposite) flippedNormals.add(index); - } + boolean fast = cl.hasOption("fast"); + int numThreads = cl.hasOption("numThreads") ? Integer.parseInt(cl.getOptionValue("numThreads")) : 1; + try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) { + List>> futures = new ArrayList<>(); - return flippedNormals; - } - } + long numFacets = polyData.GetNumberOfCells() / numThreads; + for (int i = 0; i < numThreads; i++) { + long fromIndex = i * numFacets; + long toIndex = Math.min(polyData.GetNumberOfCells(), fromIndex + numFacets); - public void flipNormals(Collection facets) { - vtkCellArray cells = new vtkCellArray(); - for (long i = 0; i < polyData.GetNumberOfCells(); ++i) { - vtkIdList idList = new vtkIdList(); - polyData.GetCellPoints(i, idList); - if (facets.contains(i)) { - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - idList.SetId(0, id0); - idList.SetId(1, id2); - idList.SetId(2, id1); - } - cells.InsertNextCell(idList); - } - polyData.SetPolys(cells); - } + FlippedNormalFinder fnf = app.new FlippedNormalFinder(fromIndex, toIndex, fast); + futures.add(executor.submit(fnf)); + } - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new ValidateNormals(); + for (Future> future : futures) flippedNormals.addAll(future.get()); - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info("{} {}", ml.label, startupMessages.get(ml)); - - NativeLibraryLoader.loadVtkLibraries(); - - // PolyDataUtil's OBJ reader messes with the normals - not reliable for a local obj - vtkOBJReader smallBodyReader = new vtkOBJReader(); - smallBodyReader.SetFileName(cl.getOptionValue("obj")); - smallBodyReader.Update(); - vtkPolyData polyData = new vtkPolyData(); - polyData.ShallowCopy(smallBodyReader.GetOutput()); - - smallBodyReader.Delete(); - - ValidateNormals app = new ValidateNormals(polyData); - - logger.info("Read {} facets from {}", polyData.GetNumberOfCells(), cl.getOptionValue("obj")); - - if (cl.hasOption("origin")) { - String[] parts = cl.getOptionValue("origin").split(","); - double[] origin = new double[3]; - for (int i = 0; i < 3; i++) origin[i] = Double.parseDouble(parts[i]); - app.setOrigin(origin); - } - - Set 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)) { - List>> futures = new ArrayList<>(); - - long numFacets = polyData.GetNumberOfCells() / numThreads; - for (int i = 0; i < numThreads; i++) { - long fromIndex = i * numFacets; - long toIndex = Math.min(polyData.GetNumberOfCells(), fromIndex + numFacets); - - FlippedNormalFinder fnf = app.new FlippedNormalFinder(fromIndex, toIndex, fast); - futures.add(executor.submit(fnf)); - } - - for (Future> future : futures) flippedNormals.addAll(future.get()); - - executor.shutdown(); - } - - logger.info( - "Found {} flipped normals out of {} facets", - flippedNormals.size(), - polyData.GetNumberOfCells()); - - if (cl.hasOption("output")) { - NavigableSet sorted = new TreeSet<>(flippedNormals); - String header = ""; - if (!flippedNormals.isEmpty()) { - header = "# The following indices were flipped from " + cl.getOptionValue("obj") + ":\n"; - StringBuilder sb = new StringBuilder("# "); - for (Long index : sorted) { - sb.append(String.format("%d", index)); - if (index < sorted.last()) sb.append(", "); + executor.shutdown(); } - sb.append("\n"); - header += WordUtils.wrap(sb.toString(), 80, "\n# ", false); - logger.info(header); - } - app.flipNormals(flippedNormals); - PolyDataUtil.saveShapeModelAsOBJ(app.polyData, cl.getOptionValue("output"), header); - logger.info("wrote OBJ file {}", cl.getOptionValue("output")); + logger.info("Found {} flipped normals out of {} facets", flippedNormals.size(), polyData.GetNumberOfCells()); + + if (cl.hasOption("output")) { + NavigableSet sorted = new TreeSet<>(flippedNormals); + String header = ""; + if (!flippedNormals.isEmpty()) { + header = "# The following indices were flipped from " + cl.getOptionValue("obj") + ":\n"; + StringBuilder sb = new StringBuilder("# "); + for (Long index : sorted) { + sb.append(String.format("%d", index)); + if (index < sorted.last()) sb.append(", "); + } + sb.append("\n"); + header += WordUtils.wrap(sb.toString(), 80, "\n# ", false); + logger.info(header); + } + + app.flipNormals(flippedNormals); + PolyDataUtil.saveShapeModelAsOBJ(app.polyData, cl.getOptionValue("output"), header); + logger.info("wrote OBJ file {}", cl.getOptionValue("output")); + } + + logger.info("ValidateNormals done"); } - - logger.info("ValidateNormals done"); - } } diff --git a/src/main/java/terrasaur/apps/ValidateOBJ.java b/src/main/java/terrasaur/apps/ValidateOBJ.java index d720480..8ced7c6 100644 --- a/src/main/java/terrasaur/apps/ValidateOBJ.java +++ b/src/main/java/terrasaur/apps/ValidateOBJ.java @@ -51,374 +51,361 @@ import vtk.vtkPolyData; */ public class ValidateOBJ implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @Override - public String shortDescription() { - return "Check a closed shape file in OBJ format for errors."; - } + @Override + public String shortDescription() { + return "Check a closed shape file in OBJ format for errors."; + } - @Override - public String fullDescription(Options options) { - String header = ""; - String footer = - """ + @Override + public String fullDescription(Options options) { + String header = ""; + String footer = + """ This program checks that a shape model has the correct number of facets and vertices. \ It will also check for duplicate vertices, vertices that are not referenced by any facet, and zero area facets. """; - return TerrasaurTool.super.fullDescription(options, header, footer); - } - - private vtkPolyData polyData; - private String validationMsg; - - private ValidateOBJ() {} - - public ValidateOBJ(vtkPolyData polyData) { - this.polyData = polyData; - } - - /** - * @return {@link vtkPolyData#GetNumberOfCells()} - */ - public long facetCount() { - return polyData.GetNumberOfCells(); - } - - /** - * @return {@link vtkPolyData#GetNumberOfPoints()} - */ - public long vertexCount() { - return polyData.GetNumberOfPoints(); - } - - /** - * @return description of test result - */ - public String getMessage() { - return validationMsg; - } - - /** - * @return true if number of facets in the shape model satisfies 3*4^n where n is an integer - */ - public boolean testFacets() { - boolean meetsCondition = facetCount() % 3 == 0; - - if (meetsCondition) { - long facet3 = facetCount() / 3; - double logFacet3 = Math.log(facet3) / Math.log(4); - if (Math.ceil(logFacet3) != Math.floor(logFacet3)) meetsCondition = false; + return TerrasaurTool.super.fullDescription(options, header, footer); } - int n = (int) (Math.log(facetCount() / 3.) / Math.log(4.0) + 0.5); - if (meetsCondition) { - validationMsg = - String.format( - "Model has %d facets. This satisfies f = 3*4^n with n = %d.", facetCount(), n); - } else { - validationMsg = - String.format( - "Model has %d facets. This does not satisfy f = 3*4^n. A shape model with %.0f facets has n = %d.", - facetCount(), 3 * Math.pow(4, n), n); + private vtkPolyData polyData; + private String validationMsg; + + private ValidateOBJ() {} + + public ValidateOBJ(vtkPolyData polyData) { + this.polyData = polyData; } - return meetsCondition; - } - - /** - * @return true if number of vertices in the shape model satisfies v=f/2+2 - */ - public boolean testVertices() { - boolean meetsCondition = (facetCount() + 4) / 2 == vertexCount(); - if (meetsCondition) - validationMsg = - String.format( - "Model has %d vertices and %d facets. This satisfies v = f/2+2.", - vertexCount(), facetCount()); - else - validationMsg = - String.format( - "Model has %d vertices and %d facets. This does not satisfy v = f/2+2. Number of vertices should be %d.", - vertexCount(), facetCount(), facetCount() / 2 + 2); - - return meetsCondition; - } - - /** - * @return key is vertex id, value is a list of vertices within a hard coded distance of 1e-10. - */ - public NavigableMap> findDuplicateVertices() { - - SmallBodyModel sbm = new SmallBodyModel(polyData); - - double[] iPt = new double[3]; - - NavigableMap> map = new TreeMap<>(); - double tol = 1e-10; - for (long i = 0; i < vertexCount(); i++) { - polyData.GetPoint(i, iPt); - - List closestVertices = new ArrayList<>(); - for (Long id : sbm.findClosestVerticesWithinRadius(iPt, tol)) - if (id > i) closestVertices.add(id); - if (!closestVertices.isEmpty()) map.put(i, closestVertices); - - if (map.containsKey(i) && !map.get(i).isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("Duplicates for vertex %d: ", i + 1)); - for (Long dupId : map.get(i)) sb.append(String.format("%d ", dupId + 1)); - logger.debug(sb.toString()); - } + /** + * @return {@link vtkPolyData#GetNumberOfCells()} + */ + public long facetCount() { + return polyData.GetNumberOfCells(); } - validationMsg = String.format("%d vertices have duplicates", map.size()); + /** + * @return {@link vtkPolyData#GetNumberOfPoints()} + */ + public long vertexCount() { + return polyData.GetNumberOfPoints(); + } - return map; - } + /** + * @return description of test result + */ + public String getMessage() { + return validationMsg; + } - /** - * @return a list of vertex indices where one or more of the coordinates fail {@link - * Double#isFinite(double)}. - */ - public List findMalformedVertices() { - double[] iPt = new double[3]; - NavigableSet vertexIndices = new TreeSet<>(); - for (int i = 0; i < vertexCount(); i++) { - polyData.GetPoint(i, iPt); - for (int j = 0; j < 3; j++) { - if (!Double.isFinite(iPt[j])) { - logger.debug("Vertex {}: {} {} {}", i, iPt[0], iPt[1], iPt[2]); - vertexIndices.add(i); - break; + /** + * @return true if number of facets in the shape model satisfies 3*4^n where n is an integer + */ + public boolean testFacets() { + boolean meetsCondition = facetCount() % 3 == 0; + + if (meetsCondition) { + long facet3 = facetCount() / 3; + double logFacet3 = Math.log(facet3) / Math.log(4); + if (Math.ceil(logFacet3) != Math.floor(logFacet3)) meetsCondition = false; } - } - } - validationMsg = String.format("%d malformed vertices ", vertexIndices.size()); - return new ArrayList<>(vertexIndices); - } - /** - * @return a list of vertex indices that are not referenced by any facet - */ - public List findUnreferencedVertices() { - NavigableSet vertexIndices = new TreeSet<>(); - for (long i = 0; i < polyData.GetNumberOfPoints(); i++) { - vertexIndices.add(i); + int n = (int) (Math.log(facetCount() / 3.) / Math.log(4.0) + 0.5); + if (meetsCondition) { + validationMsg = + String.format("Model has %d facets. This satisfies f = 3*4^n with n = %d.", facetCount(), n); + } else { + validationMsg = String.format( + "Model has %d facets. This does not satisfy f = 3*4^n. A shape model with %.0f facets has n = %d.", + facetCount(), 3 * Math.pow(4, n), n); + } + + return meetsCondition; } - vtkIdList idList = new vtkIdList(); - for (int i = 0; i < facetCount(); ++i) { - polyData.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); + /** + * @return true if number of vertices in the shape model satisfies v=f/2+2 + */ + public boolean testVertices() { + boolean meetsCondition = (facetCount() + 4) / 2 == vertexCount(); + if (meetsCondition) + validationMsg = String.format( + "Model has %d vertices and %d facets. This satisfies v = f/2+2.", vertexCount(), facetCount()); + else + validationMsg = String.format( + "Model has %d vertices and %d facets. This does not satisfy v = f/2+2. Number of vertices should be %d.", + vertexCount(), facetCount(), facetCount() / 2 + 2); - vertexIndices.remove(id0); - vertexIndices.remove(id1); - vertexIndices.remove(id2); + return meetsCondition; } - if (!vertexIndices.isEmpty()) { - double[] pt = new double[3]; - for (long id : vertexIndices) { - polyData.GetPoint(id, pt); - logger.debug("Unreferenced vertex {} [{}, {}, {}]", id + 1, pt[0], pt[1], pt[2]); - // note OBJ vertices are numbered from 1 but VTK uses 0 - } + /** + * @return key is vertex id, value is a list of vertices within a hard coded distance of 1e-10. + */ + public NavigableMap> findDuplicateVertices() { + + SmallBodyModel sbm = new SmallBodyModel(polyData); + + double[] iPt = new double[3]; + + NavigableMap> map = new TreeMap<>(); + double tol = 1e-10; + for (long i = 0; i < vertexCount(); i++) { + polyData.GetPoint(i, iPt); + + List closestVertices = new ArrayList<>(); + for (Long id : sbm.findClosestVerticesWithinRadius(iPt, tol)) if (id > i) closestVertices.add(id); + if (!closestVertices.isEmpty()) map.put(i, closestVertices); + + if (map.containsKey(i) && !map.get(i).isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Duplicates for vertex %d: ", i + 1)); + for (Long dupId : map.get(i)) sb.append(String.format("%d ", dupId + 1)); + logger.debug(sb.toString()); + } + } + + validationMsg = String.format("%d vertices have duplicates", map.size()); + + return map; } - validationMsg = String.format("%d unreferenced vertices found", vertexIndices.size()); - - return new ArrayList<>(vertexIndices); - } - - /** - * @return a list of facet indices where the facet has zero area - */ - public List findZeroAreaFacets() { - List zeroAreaFacets = new ArrayList<>(); - vtkIdList idList = new vtkIdList(); - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; - - for (int i = 0; i < facetCount(); ++i) { - polyData.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - polyData.GetPoint(id0, pt0); - polyData.GetPoint(id1, pt1); - polyData.GetPoint(id2, pt2); - - // would be faster to check if id0==id1||id0==id2||id1==id2 but there may be - // duplicate vertices - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - double area = facet.getArea(); - if (area == 0) { - zeroAreaFacets.add(i); - logger.debug( - "Facet {} has zero area. Vertices are {} [{}, {}, {}], {} [{}, {}, {}], and {} [{}, {}, {}]", - i + 1, - id0 + 1, - pt0[0], - pt0[1], - pt0[2], - id1 + 1, - pt1[0], - pt1[1], - pt1[2], - id2 + 1, - pt2[0], - pt2[1], - pt2[2]); - } + /** + * @return a list of vertex indices where one or more of the coordinates fail {@link + * Double#isFinite(double)}. + */ + public List findMalformedVertices() { + double[] iPt = new double[3]; + NavigableSet vertexIndices = new TreeSet<>(); + for (int i = 0; i < vertexCount(); i++) { + polyData.GetPoint(i, iPt); + for (int j = 0; j < 3; j++) { + if (!Double.isFinite(iPt[j])) { + logger.debug("Vertex {}: {} {} {}", i, iPt[0], iPt[1], iPt[2]); + vertexIndices.add(i); + break; + } + } + } + validationMsg = String.format("%d malformed vertices ", vertexIndices.size()); + return new ArrayList<>(vertexIndices); } - validationMsg = String.format("%d zero area facets found", zeroAreaFacets.size()); - return zeroAreaFacets; - } + /** + * @return a list of vertex indices that are not referenced by any facet + */ + public List findUnreferencedVertices() { + NavigableSet vertexIndices = new TreeSet<>(); + for (long i = 0; i < polyData.GetNumberOfPoints(); i++) { + vertexIndices.add(i); + } - /** - * @return statistics on the angle between the facet radial and normal vectors - */ - public DescriptiveStatistics normalStats() { - DescriptiveStatistics stats = new DescriptiveStatistics(); + vtkIdList idList = new vtkIdList(); + for (int i = 0; i < facetCount(); ++i) { + polyData.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); - VectorStatistics cStats = new VectorStatistics(); - VectorStatistics nStats = new VectorStatistics(); + vertexIndices.remove(id0); + vertexIndices.remove(id1); + vertexIndices.remove(id2); + } - vtkIdList idList = new vtkIdList(); - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; - for (int i = 0; i < facetCount(); ++i) { - polyData.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - polyData.GetPoint(id0, pt0); - polyData.GetPoint(id1, pt1); - polyData.GetPoint(id2, pt2); + if (!vertexIndices.isEmpty()) { + double[] pt = new double[3]; + for (long id : vertexIndices) { + polyData.GetPoint(id, pt); + logger.debug("Unreferenced vertex {} [{}, {}, {}]", id + 1, pt[0], pt[1], pt[2]); + // note OBJ vertices are numbered from 1 but VTK uses 0 + } + } - // would be faster to check if id0==id1||id0==id2||id1==id2 but there may be - // duplicate vertices - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - if (facet.getArea() > 0) { - stats.addValue(facet.getCenter().createUnitized().getDot(facet.getNormal())); - cStats.add(facet.getCenter()); - nStats.add(facet.getNormal()); - } + validationMsg = String.format("%d unreferenced vertices found", vertexIndices.size()); + + return new ArrayList<>(vertexIndices); } - validationMsg = - String.format( - "Using %d non-zero area facets: Mean angle between radial and normal is %f degrees, " - + "angle between mean radial and mean normal is %f degrees", - stats.getN(), - Math.toDegrees(Math.acos(stats.getMean())), - Math.toDegrees(Vector3D.angle(cStats.getMean(), nStats.getMean()))); + /** + * @return a list of facet indices where the facet has zero area + */ + public List findZeroAreaFacets() { + List zeroAreaFacets = new ArrayList<>(); + vtkIdList idList = new vtkIdList(); + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; - return stats; - } + for (int i = 0; i < facetCount(); ++i) { + polyData.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + polyData.GetPoint(id0, pt0); + polyData.GetPoint(id1, pt1); + polyData.GetPoint(id2, pt2); - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("obj").required().hasArg().desc("Shape model to validate.").build()); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - options.addOption(Option.builder("output").hasArg().desc("Filename for output OBJ.").build()); - options.addOption( - Option.builder("removeDuplicateVertices") - .desc("Remove duplicate vertices. Use with -output to save OBJ.") - .build()); - options.addOption( - Option.builder("removeUnreferencedVertices") - .desc("Remove unreferenced vertices. Use with -output to save OBJ.") - .build()); - options.addOption( - Option.builder("removeZeroAreaFacets") - .desc("Remove facets with zero area. Use with -output to save OBJ.") - .build()); - options.addOption( - Option.builder("cleanup") - .desc( - "Combines -removeDuplicateVertices, -removeUnreferencedVertices, and -removeZeroAreaFacets.") - .build()); - return options; - } + // would be faster to check if id0==id1||id0==id2||id1==id2 but there may be + // duplicate vertices + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + double area = facet.getArea(); + if (area == 0) { + zeroAreaFacets.add(i); + logger.debug( + "Facet {} has zero area. Vertices are {} [{}, {}, {}], {} [{}, {}, {}], and {} [{}, {}, {}]", + i + 1, + id0 + 1, + pt0[0], + pt0[1], + pt0[2], + id1 + 1, + pt1[0], + pt1[1], + pt1[2], + id2 + 1, + pt2[0], + pt2[1], + pt2[2]); + } + } - public static void main(String[] args) throws Exception { - TerrasaurTool defaultOBJ = new ValidateOBJ(); - - Options options = defineOptions(); - - CommandLine cl = defaultOBJ.parseArgs(args, options); - - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info("{} {}", ml.label, startupMessages.get(ml)); - - NativeLibraryLoader.loadVtkLibraries(); - vtkPolyData polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("obj")); - - if (polyData == null) { - logger.error("Cannot read {}, exiting.", cl.getOptionValue("obj")); - System.exit(0); + validationMsg = String.format("%d zero area facets found", zeroAreaFacets.size()); + return zeroAreaFacets; } - ValidateOBJ vo = new ValidateOBJ(polyData); + /** + * @return statistics on the angle between the facet radial and normal vectors + */ + public DescriptiveStatistics normalStats() { + DescriptiveStatistics stats = new DescriptiveStatistics(); - logger.log(vo.testFacets() ? Level.INFO : Level.WARN, vo.getMessage()); - logger.log(vo.testVertices() ? Level.INFO : Level.WARN, vo.getMessage()); + VectorStatistics cStats = new VectorStatistics(); + VectorStatistics nStats = new VectorStatistics(); - DescriptiveStatistics stats = vo.normalStats(); - logger.log(stats.getMean() > 0 ? Level.INFO : Level.WARN, vo.getMessage()); + vtkIdList idList = new vtkIdList(); + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; + for (int i = 0; i < facetCount(); ++i) { + polyData.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + polyData.GetPoint(id0, pt0); + polyData.GetPoint(id1, pt1); + polyData.GetPoint(id2, pt2); - List mfv = vo.findMalformedVertices(); - logger.log(!mfv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + // would be faster to check if id0==id1||id0==id2||id1==id2 but there may be + // duplicate vertices + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + if (facet.getArea() > 0) { + stats.addValue(facet.getCenter().createUnitized().getDot(facet.getNormal())); + cStats.add(facet.getCenter()); + nStats.add(facet.getNormal()); + } + } - NavigableMap> dv = vo.findDuplicateVertices(); - logger.log(!dv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + validationMsg = String.format( + "Using %d non-zero area facets: Mean angle between radial and normal is %f degrees, " + + "angle between mean radial and mean normal is %f degrees", + stats.getN(), + Math.toDegrees(Math.acos(stats.getMean())), + Math.toDegrees(Vector3D.angle(cStats.getMean(), nStats.getMean()))); - List urv = vo.findUnreferencedVertices(); - logger.log(!urv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); - - List zaf = vo.findZeroAreaFacets(); - logger.log(!zaf.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); - - final boolean cleanup = cl.hasOption("cleanup"); - final boolean removeDuplicateVertices = cleanup || cl.hasOption("removeDuplicateVertices"); - final boolean removeUnreferencedVertices = - cleanup || cl.hasOption("removeUnreferencedVertices"); - final boolean removeZeroAreaFacets = cleanup || cl.hasOption("removeZeroAreaFacets"); - - if (removeDuplicateVertices) polyData = PolyDataUtil.removeDuplicatePoints(polyData); - - if (removeUnreferencedVertices) polyData = PolyDataUtil.removeUnreferencedPoints(polyData); - - if (removeZeroAreaFacets) polyData = PolyDataUtil.removeZeroAreaFacets(polyData); - - if (cl.hasOption("output")) { - PolyDataUtil.saveShapeModelAsOBJ(polyData, cl.getOptionValue("output")); - logger.info("Wrote OBJ file {}", cl.getOptionValue("output")); + return stats; + } + + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("obj") + .required() + .hasArg() + .desc("Shape model to validate.") + .build()); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + options.addOption(Option.builder("output") + .hasArg() + .desc("Filename for output OBJ.") + .build()); + options.addOption(Option.builder("removeDuplicateVertices") + .desc("Remove duplicate vertices. Use with -output to save OBJ.") + .build()); + options.addOption(Option.builder("removeUnreferencedVertices") + .desc("Remove unreferenced vertices. Use with -output to save OBJ.") + .build()); + options.addOption(Option.builder("removeZeroAreaFacets") + .desc("Remove facets with zero area. Use with -output to save OBJ.") + .build()); + options.addOption(Option.builder("cleanup") + .desc("Combines -removeDuplicateVertices, -removeUnreferencedVertices, and -removeZeroAreaFacets.") + .build()); + return options; + } + + public static void main(String[] args) throws Exception { + TerrasaurTool defaultOBJ = new ValidateOBJ(); + + Options options = defineOptions(); + + CommandLine cl = defaultOBJ.parseArgs(args, options); + + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) logger.info("{} {}", ml.label, startupMessages.get(ml)); + + NativeLibraryLoader.loadVtkLibraries(); + vtkPolyData polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("obj")); + + if (polyData == null) { + logger.error("Cannot read {}, exiting.", cl.getOptionValue("obj")); + System.exit(0); + } + + ValidateOBJ vo = new ValidateOBJ(polyData); + + logger.log(vo.testFacets() ? Level.INFO : Level.WARN, vo.getMessage()); + logger.log(vo.testVertices() ? Level.INFO : Level.WARN, vo.getMessage()); + + DescriptiveStatistics stats = vo.normalStats(); + logger.log(stats.getMean() > 0 ? Level.INFO : Level.WARN, vo.getMessage()); + + List mfv = vo.findMalformedVertices(); + logger.log(!mfv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + + NavigableMap> dv = vo.findDuplicateVertices(); + logger.log(!dv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + + List urv = vo.findUnreferencedVertices(); + logger.log(!urv.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + + List zaf = vo.findZeroAreaFacets(); + logger.log(!zaf.isEmpty() ? Level.WARN : Level.INFO, vo.getMessage()); + + final boolean cleanup = cl.hasOption("cleanup"); + final boolean removeDuplicateVertices = cleanup || cl.hasOption("removeDuplicateVertices"); + final boolean removeUnreferencedVertices = cleanup || cl.hasOption("removeUnreferencedVertices"); + final boolean removeZeroAreaFacets = cleanup || cl.hasOption("removeZeroAreaFacets"); + + if (removeDuplicateVertices) polyData = PolyDataUtil.removeDuplicatePoints(polyData); + + if (removeUnreferencedVertices) polyData = PolyDataUtil.removeUnreferencedPoints(polyData); + + if (removeZeroAreaFacets) polyData = PolyDataUtil.removeZeroAreaFacets(polyData); + + if (cl.hasOption("output")) { + PolyDataUtil.saveShapeModelAsOBJ(polyData, cl.getOptionValue("output")); + logger.info("Wrote OBJ file {}", cl.getOptionValue("output")); + } } - } } diff --git a/src/main/java/terrasaur/config/CKFromSumFileConfig.java b/src/main/java/terrasaur/config/CKFromSumFileConfig.java index 0378103..e1127a6 100644 --- a/src/main/java/terrasaur/config/CKFromSumFileConfig.java +++ b/src/main/java/terrasaur/config/CKFromSumFileConfig.java @@ -22,33 +22,36 @@ */ package terrasaur.config; -import java.util.List; import jackfruit.annotations.Comment; import jackfruit.annotations.DefaultValue; import jackfruit.annotations.Jackfruit; +import java.util.List; @Jackfruit public interface CKFromSumFileConfig { - @Comment(""" + @Comment( + """ Body fixed frame for the target body. If blank, use SPICE-defined body fixed frame. This will be the reference frame unless the J2000 parameter is set to true.""") - @DefaultValue("IAU_DIMORPHOS") - String bodyFrame(); + @DefaultValue("IAU_DIMORPHOS") + String bodyFrame(); - @Comment("Target body name.") - @DefaultValue("DIMORPHOS") - String bodyName(); + @Comment("Target body name.") + @DefaultValue("DIMORPHOS") + String bodyName(); - @Comment(""" + @Comment( + """ Extend CK past the last sumFile by this number of seconds. Default is zero. Attitude is assumed to be fixed to the value given by the last sumfile.""") - @DefaultValue("0") - double extend(); + @DefaultValue("0") + double extend(); - @Comment(""" + @Comment( + """ SPC defines the camera X axis to be increasing to the right, Y to be increasing down, and Z to point into the page: @@ -74,48 +77,48 @@ public interface CKFromSumFileConfig { (flipX, flipY, flipZ) = ( 2,-1, 3) SPICE frame is camera frame rotated 90 degrees about Z. (flipX, flipY, flipZ) = (-2, 1, 3) SPICE frame is camera frame rotated -90 degrees about Z. (flipX, flipY, flipZ) = ( 1,-2,-3) rotates the image 180 degrees about X.""") - @DefaultValue("-1") - int flipX(); + @DefaultValue("-1") + int flipX(); - @Comment("Map the camera Y axis to a SPICE axis. See flipX for details.") - @DefaultValue("2") - int flipY(); + @Comment("Map the camera Y axis to a SPICE axis. See flipX for details.") + @DefaultValue("2") + int flipY(); - @Comment("Map the camera Z axis to a SPICE axis. See flipX for details.") - @DefaultValue("-3") - int flipZ(); + @Comment("Map the camera Z axis to a SPICE axis. See flipX for details.") + @DefaultValue("-3") + int flipZ(); - @Comment(""" + @Comment( + """ Supply this frame kernel to MSOPCK. Only needed if the reference frame (set by bodyFrame or J2000) is not built into SPICE""") - @DefaultValue("/project/dart/data/SPICE/flight/fk/didymos_system_001.tf") - String fk(); + @DefaultValue("/project/dart/data/SPICE/flight/fk/didymos_system_001.tf") + String fk(); - @Comment("Instrument frame name") - @DefaultValue("DART_DRACO") - String instrumentFrameName(); + @Comment("Instrument frame name") + @DefaultValue("DART_DRACO") + String instrumentFrameName(); - @Comment("If set to true, use J2000 as the reference frame") - @DefaultValue("true") - boolean J2000(); + @Comment("If set to true, use J2000 as the reference frame") + @DefaultValue("true") + boolean J2000(); - @Comment("Path to leapseconds kernel.") - @DefaultValue("/project/dart/data/SPICE/flight/lsk/naif0012.tls") - String lsk(); + @Comment("Path to leapseconds kernel.") + @DefaultValue("/project/dart/data/SPICE/flight/lsk/naif0012.tls") + String lsk(); - @Comment("Path to spacecraft SCLK file.") - @DefaultValue("/project/dart/data/SPICE/flight/sclk/dart_sclk_0204.tsc") - String sclk(); + @Comment("Path to spacecraft SCLK file.") + @DefaultValue("/project/dart/data/SPICE/flight/sclk/dart_sclk_0204.tsc") + String sclk(); - @Comment("Name of spacecraft frame.") - @DefaultValue("DART_SPACECRAFT") - String spacecraftFrame(); + @Comment("Name of spacecraft frame.") + @DefaultValue("DART_SPACECRAFT") + String spacecraftFrame(); - @Comment(""" + @Comment( + """ SPICE metakernel to read. This may be specified more than once for multiple metakernels.""") - @DefaultValue("/project/dart/data/SPICE/flight/mk/current.tm") - List metakernel(); - - + @DefaultValue("/project/dart/data/SPICE/flight/mk/current.tm") + List metakernel(); } diff --git a/src/main/java/terrasaur/config/CommandLineOptions.java b/src/main/java/terrasaur/config/CommandLineOptions.java index 7ee4dc7..4e4c35f 100644 --- a/src/main/java/terrasaur/config/CommandLineOptions.java +++ b/src/main/java/terrasaur/config/CommandLineOptions.java @@ -29,230 +29,233 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.spi.StandardLevel; -import terrasaur.utils.saaPlotLib.colorMaps.ColorRamp; import terrasaur.utils.Log4j2Configurator; +import terrasaur.utils.saaPlotLib.colorMaps.ColorRamp; public class CommandLineOptions { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - /** - * Configuration file to load - * - * @return - */ - public static Option addConfig() { - return Option.builder("config").hasArg().required().desc("Configuration file to load").build(); - } + /** + * Configuration file to load + * + * @return + */ + public static Option addConfig() { + return Option.builder("config") + .hasArg() + .required() + .desc("Configuration file to load") + .build(); + } - /** - * Color ramp style. See {@link ColorRamp.TYPE} for valid values. - * - * @param defaultCRType - * @return - */ - public static Option addColorRamp(ColorRamp.TYPE defaultCRType) { - StringBuilder sb = new StringBuilder(); - for (ColorRamp.TYPE t : ColorRamp.TYPE.values()) sb.append(String.format("%s ", t.name())); - return Option.builder("colorRamp") - .hasArg() - .desc( - "Color ramp style. Valid values are " - + sb.toString().trim() - + ". Default is " - + defaultCRType.name() - + ". Run the ColorMaps application to see all supported color ramps.") - .build(); - } + /** + * Color ramp style. See {@link ColorRamp.TYPE} for valid values. + * + * @param defaultCRType + * @return + */ + public static Option addColorRamp(ColorRamp.TYPE defaultCRType) { + StringBuilder sb = new StringBuilder(); + for (ColorRamp.TYPE t : ColorRamp.TYPE.values()) sb.append(String.format("%s ", t.name())); + return Option.builder("colorRamp") + .hasArg() + .desc("Color ramp style. Valid values are " + + sb.toString().trim() + + ". Default is " + + defaultCRType.name() + + ". Run the ColorMaps application to see all supported color ramps.") + .build(); + } - /** - * Return a color ramp type or the default value. - * - * @param cl - * @param defaultCRType - * @return - */ - public static ColorRamp.TYPE getColorRamp(CommandLine cl, ColorRamp.TYPE defaultCRType) { - ColorRamp.TYPE crType = - cl.hasOption("colorRamp") - ? ColorRamp.TYPE.valueOf(cl.getOptionValue("colorRamp").toUpperCase().strip()) - : defaultCRType; - return crType; - } + /** + * Return a color ramp type or the default value. + * + * @param cl + * @param defaultCRType + * @return + */ + public static ColorRamp.TYPE getColorRamp(CommandLine cl, ColorRamp.TYPE defaultCRType) { + ColorRamp.TYPE crType = cl.hasOption("colorRamp") + ? ColorRamp.TYPE.valueOf( + cl.getOptionValue("colorRamp").toUpperCase().strip()) + : defaultCRType; + return crType; + } - /** - * Hard lower limit for color bar. If the color bar minimum is set dynamically it will not be - * lower than hardMin. - * - * @return - */ - public static Option addHardMin() { - return Option.builder("hardMin") - .hasArg() - .desc( - "Hard lower limit for color bar. If the color bar minimum is set dynamically it will not be lower than hardMin.") - .build(); - } + /** + * Hard lower limit for color bar. If the color bar minimum is set dynamically it will not be + * lower than hardMin. + * + * @return + */ + public static Option addHardMin() { + return Option.builder("hardMin") + .hasArg() + .desc( + "Hard lower limit for color bar. If the color bar minimum is set dynamically it will not be lower than hardMin.") + .build(); + } - /** - * Hard upper limit for color bar. If the color bar maximum is set dynamically it will not be - * higher than hardMax. - * - * @return - */ - public static Option addHardMax() { - return Option.builder("hardMax") - .hasArg() - .desc( - "Hard upper limit for color bar. If the color bar maximum is set dynamically it will not be higher than hardMax.") - .build(); - } + /** + * Hard upper limit for color bar. If the color bar maximum is set dynamically it will not be + * higher than hardMax. + * + * @return + */ + public static Option addHardMax() { + return Option.builder("hardMax") + .hasArg() + .desc( + "Hard upper limit for color bar. If the color bar maximum is set dynamically it will not be higher than hardMax.") + .build(); + } - /** - * Get the hard minimum for the colorbar. - * - * @param cl - * @return - */ - public static double getHardMin(CommandLine cl) { - return cl.hasOption("hardMin") ? Double.parseDouble(cl.getOptionValue("hardMin")) : Double.NaN; - } + /** + * Get the hard minimum for the colorbar. + * + * @param cl + * @return + */ + public static double getHardMin(CommandLine cl) { + return cl.hasOption("hardMin") ? Double.parseDouble(cl.getOptionValue("hardMin")) : Double.NaN; + } - /** - * Get the hard maximum for the colorbar. - * - * @param cl - * @return - */ - public static double getHardMax(CommandLine cl) { - return cl.hasOption("hardMax") ? Double.parseDouble(cl.getOptionValue("hardMax")) : Double.NaN; - } + /** + * Get the hard maximum for the colorbar. + * + * @param cl + * @return + */ + public static double getHardMax(CommandLine cl) { + return cl.hasOption("hardMax") ? Double.parseDouble(cl.getOptionValue("hardMax")) : Double.NaN; + } - /** - * If present, save screen output to log file. - * - * @return - */ - public static Option addLogFile() { - return Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build(); - } + /** + * If present, save screen output to log file. + * + * @return + */ + public static Option addLogFile() { + return Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build(); + } - /** - * If present, print messages above selected priority. See {@link StandardLevel} for valid values. - * - * @return - */ - public static Option addLogLevel() { - StringBuilder sb = new StringBuilder(); - for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); - return Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build(); - } + /** + * If present, print messages above selected priority. See {@link StandardLevel} for valid values. + * + * @return + */ + public static Option addLogLevel() { + StringBuilder sb = new StringBuilder(); + for (StandardLevel l : StandardLevel.values()) sb.append(String.format("%s ", l.name())); + return Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build(); + } - /** - * Set the logging level from the command line option. - * - * @param cl - */ - public static void setLogLevel(CommandLine cl) { - Log4j2Configurator lc = Log4j2Configurator.getInstance(); - if (cl.hasOption("logLevel")) - lc.setLevel(Level.valueOf(cl.getOptionValue("logLevel").toUpperCase().trim())); - } + /** + * Set the logging level from the command line option. + * + * @param cl + */ + public static void setLogLevel(CommandLine cl) { + Log4j2Configurator lc = Log4j2Configurator.getInstance(); + if (cl.hasOption("logLevel")) + lc.setLevel( + Level.valueOf(cl.getOptionValue("logLevel").toUpperCase().trim())); + } - /** - * Log to file named on the command line as well as others - * - * @param cl - * @param others - */ - public static void setLogFile(CommandLine cl, String... others) { - Log4j2Configurator lc = Log4j2Configurator.getInstance(); - if (cl.hasOption("logFile")) lc.addFile(cl.getOptionValue("logFile")); - for (String other : others) lc.addFile(other); - } + /** + * Log to file named on the command line as well as others + * + * @param cl + * @param others + */ + public static void setLogFile(CommandLine cl, String... others) { + Log4j2Configurator lc = Log4j2Configurator.getInstance(); + if (cl.hasOption("logFile")) lc.addFile(cl.getOptionValue("logFile")); + for (String other : others) lc.addFile(other); + } - /** - * Maximum number of simultaneous threads to execute. - * - * @return - */ - public static Option addNumCPU() { - return Option.builder("numCPU") - .hasArg() - .desc( - "Maximum number of simultaneous threads to execute. Default is numCPU value in configuration file.") - .build(); - } + /** + * Maximum number of simultaneous threads to execute. + * + * @return + */ + public static Option addNumCPU() { + return Option.builder("numCPU") + .hasArg() + .desc( + "Maximum number of simultaneous threads to execute. Default is numCPU value in configuration file.") + .build(); + } - /** - * Directory to place output files. Default is the working directory. - * - * @return - */ - public static Option addOutputDir() { - return Option.builder("outputDir") - .hasArg() - .desc("Directory to place output files. Default is the working directory.") - .build(); - } + /** + * Directory to place output files. Default is the working directory. + * + * @return + */ + public static Option addOutputDir() { + return Option.builder("outputDir") + .hasArg() + .desc("Directory to place output files. Default is the working directory.") + .build(); + } - /** - * Set the output dir from the command line argument. - * - * @param cl - * @return - */ - public static String setOutputDir(CommandLine cl) { - String path = cl.hasOption("outputDir") ? cl.getOptionValue("outputDir") : "."; - File parent = new File(path); - if (!parent.exists()) parent.mkdirs(); - return path; - } + /** + * Set the output dir from the command line argument. + * + * @param cl + * @return + */ + public static String setOutputDir(CommandLine cl) { + String path = cl.hasOption("outputDir") ? cl.getOptionValue("outputDir") : "."; + File parent = new File(path); + if (!parent.exists()) parent.mkdirs(); + return path; + } - /** - * Minimum value to plot. - * - * @return - */ - public static Option addPlotMin() { - return Option.builder("plotMin").hasArg().desc("Min value to plot.").build(); - } + /** + * Minimum value to plot. + * + * @return + */ + public static Option addPlotMin() { + return Option.builder("plotMin").hasArg().desc("Min value to plot.").build(); + } - /** - * Get plot min from command line argument - * - * @param cl - * @return - */ - public static double getPlotMin(CommandLine cl) { - return cl.hasOption("plotMin") ? Double.parseDouble(cl.getOptionValue("plotMin")) : Double.NaN; - } + /** + * Get plot min from command line argument + * + * @param cl + * @return + */ + public static double getPlotMin(CommandLine cl) { + return cl.hasOption("plotMin") ? Double.parseDouble(cl.getOptionValue("plotMin")) : Double.NaN; + } - /** - * Maximum value to plot. - * - * @return - */ - public static Option addPlotMax() { - return Option.builder("plotMax").hasArg().desc("Max value to plot.").build(); - } + /** + * Maximum value to plot. + * + * @return + */ + public static Option addPlotMax() { + return Option.builder("plotMax").hasArg().desc("Max value to plot.").build(); + } - /** - * Get plot max from command line argument - * - * @param cl - * @return - */ - public static double getPlotMax(CommandLine cl) { - return cl.hasOption("plotMax") ? Double.parseDouble(cl.getOptionValue("plotMax")) : Double.NaN; - } + /** + * Get plot max from command line argument + * + * @param cl + * @return + */ + public static double getPlotMax(CommandLine cl) { + return cl.hasOption("plotMax") ? Double.parseDouble(cl.getOptionValue("plotMax")) : Double.NaN; + } } diff --git a/src/main/java/terrasaur/config/ConfigBlock.java b/src/main/java/terrasaur/config/ConfigBlock.java index 945f4cf..318022e 100644 --- a/src/main/java/terrasaur/config/ConfigBlock.java +++ b/src/main/java/terrasaur/config/ConfigBlock.java @@ -30,16 +30,16 @@ import jackfruit.annotations.Jackfruit; @Jackfruit public interface ConfigBlock { - String introLines = - """ + String introLines = + """ ############################################################################### # GENERAL PARAMETERS ############################################################################### """; - @Comment( - introLines - + """ + @Comment( + introLines + + """ Set the logging level. Valid values in order of increasing detail: OFF FATAL @@ -50,28 +50,28 @@ public interface ConfigBlock { TRACE ALL See org.apache.logging.log4j.Level.""") - @DefaultValue("INFO") - String logLevel(); + @DefaultValue("INFO") + String logLevel(); - @Comment( - "Format for log messages. See https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout for more details.") - @DefaultValue("%highlight{%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c{1}:%L] %msg%n%throwable}") - String logFormat(); + @Comment( + "Format for log messages. See https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout for more details.") + @DefaultValue("%highlight{%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c{1}:%L] %msg%n%throwable}") + String logFormat(); - @Comment( - """ + @Comment( + """ Format for time strings. Allowed values are: C (e.g. 1986 APR 12 16:31:09.814) D (e.g. 1986-102 // 16:31:12.814) J (e.g. 2446533.18834276) ISOC (e.g. 1986-04-12T16:31:12.814) ISOD (e.g. 1986-102T16:31:12.814)""") - @DefaultValue("ISOC") - String timeFormat(); + @DefaultValue("ISOC") + String timeFormat(); - @Include - MissionBlock missionBlock(); + @Include + MissionBlock missionBlock(); - @Include - SPCBlock spcBlock(); + @Include + SPCBlock spcBlock(); } diff --git a/src/main/java/terrasaur/config/MissionBlock.java b/src/main/java/terrasaur/config/MissionBlock.java index ba274df..7af259f 100644 --- a/src/main/java/terrasaur/config/MissionBlock.java +++ b/src/main/java/terrasaur/config/MissionBlock.java @@ -25,35 +25,34 @@ package terrasaur.config; import jackfruit.annotations.Comment; import jackfruit.annotations.DefaultValue; import jackfruit.annotations.Jackfruit; - import java.util.List; @Jackfruit(prefix = "mission") public interface MissionBlock { - String introLines = - """ + String introLines = + """ ############################################################################### # MISSION PARAMETERS ############################################################################### """; - @Comment(introLines + "Mission name (e.g. DART)") - @DefaultValue("mission") - String missionName(); + @Comment(introLines + "Mission name (e.g. DART)") + @DefaultValue("mission") + String missionName(); - @Comment( - """ + @Comment( + """ SPICE metakernel to read. This may be specified more than once for multiple metakernels (e.g. /project/dart/data/SPICE/flight/mk/current.tm)""") - @DefaultValue("metakernel.tm") - List metakernel(); + @DefaultValue("metakernel.tm") + List metakernel(); - @Comment("Name of spacecraft frame (e.g. DART_SPACECRAFT)") - @DefaultValue("SPACECRAFT_FRAME") - String spacecraftFrame(); + @Comment("Name of spacecraft frame (e.g. DART_SPACECRAFT)") + @DefaultValue("SPACECRAFT_FRAME") + String spacecraftFrame(); - @Comment("Instrument frame name (e.g. DART_DRACO)") - @DefaultValue("INSTRUMENT_FRAME") - String instrumentFrameName(); + @Comment("Instrument frame name (e.g. DART_DRACO)") + @DefaultValue("INSTRUMENT_FRAME") + String instrumentFrameName(); } diff --git a/src/main/java/terrasaur/config/SPCBlock.java b/src/main/java/terrasaur/config/SPCBlock.java index eba4a79..ae8d144 100644 --- a/src/main/java/terrasaur/config/SPCBlock.java +++ b/src/main/java/terrasaur/config/SPCBlock.java @@ -29,14 +29,16 @@ import jackfruit.annotations.Jackfruit; @Jackfruit(prefix = "spc") public interface SPCBlock { - String introLines = + String introLines = """ ############################################################################### # SPC PARAMETERS ############################################################################### """; - @Comment(introLines + """ + @Comment( + introLines + + """ SPC defines the camera X axis to be increasing to the right, Y to be increasing down, and Z to point into the page: @@ -72,5 +74,4 @@ public interface SPCBlock { @Comment("Map the camera Z axis to a SPICE axis. See flipX for details.") @DefaultValue("-3") int flipZ(); - } diff --git a/src/main/java/terrasaur/config/TerrasaurConfig.java b/src/main/java/terrasaur/config/TerrasaurConfig.java index ca8a2d6..b93ab9f 100644 --- a/src/main/java/terrasaur/config/TerrasaurConfig.java +++ b/src/main/java/terrasaur/config/TerrasaurConfig.java @@ -41,71 +41,68 @@ import terrasaur.utils.AppVersion; public class TerrasaurConfig { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private static TerrasaurConfig instance = null; + private static TerrasaurConfig instance = null; - private TerrasaurConfig() {} + private TerrasaurConfig() {} - private ConfigBlock configBlock; + private ConfigBlock configBlock; - public static ConfigBlock getConfig() { - if (instance == null) { - logger.error("Configuration has not been loaded! Returning null."); - return null; + public static ConfigBlock getConfig() { + if (instance == null) { + logger.error("Configuration has not been loaded! Returning null."); + return null; + } + return instance.configBlock; } - return instance.configBlock; - } - public static ConfigBlock getTemplate() { - if (instance == null) { - instance = new TerrasaurConfig(); - ConfigBlockFactory factory = new ConfigBlockFactory(); - instance.configBlock = factory.getTemplate(); + public static ConfigBlock getTemplate() { + if (instance == null) { + instance = new TerrasaurConfig(); + ConfigBlockFactory factory = new ConfigBlockFactory(); + instance.configBlock = factory.getTemplate(); + } + return instance.configBlock; } - return instance.configBlock; - } - public static ConfigBlock load(Path filename) { - if (!Files.exists(filename)) { - System.err.println("Cannot load configuration file " + filename); - Thread.dumpStack(); - System.exit(1); + public static ConfigBlock load(Path filename) { + if (!Files.exists(filename)) { + System.err.println("Cannot load configuration file " + filename); + Thread.dumpStack(); + System.exit(1); + } + if (instance == null) { + instance = new TerrasaurConfig(); + + try { + PropertiesConfiguration config = new Configurations().properties(filename.toFile()); + instance.configBlock = new ConfigBlockFactory().fromConfig(config); + } catch (ConfigurationException e) { + e.printStackTrace(); + } + } + return instance.configBlock; } - if (instance == null) { - instance = new TerrasaurConfig(); - try { - PropertiesConfiguration config = new Configurations().properties(filename.toFile()); - instance.configBlock = new ConfigBlockFactory().fromConfig(config); - } catch (ConfigurationException e) { - e.printStackTrace(); - } + @Override + public String toString() { + StringWriter string = new StringWriter(); + try (PrintWriter pw = new PrintWriter(string)) { + PropertiesConfiguration config = new ConfigBlockFactory().toConfig(instance.configBlock); + PropertiesConfigurationLayout layout = config.getLayout(); + + String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withLocale(Locale.getDefault()) + .withZone(ZoneOffset.UTC) + .format(Instant.now()); + layout.setHeaderComment( + String.format("Configuration file for %s\nCreated %s UTC", AppVersion.getVersionString(), now)); + + config.write(pw); + } catch (ConfigurationException | IOException e) { + e.printStackTrace(); + } + return string.toString(); } - return instance.configBlock; - } - - @Override - public String toString() { - StringWriter string = new StringWriter(); - try (PrintWriter pw = new PrintWriter(string)) { - PropertiesConfiguration config = new ConfigBlockFactory().toConfig(instance.configBlock); - PropertiesConfigurationLayout layout = config.getLayout(); - - String now = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withLocale(Locale.getDefault()) - .withZone(ZoneOffset.UTC) - .format(Instant.now()); - layout.setHeaderComment( - String.format( - "Configuration file for %s\nCreated %s UTC", AppVersion.getVersionString(), now)); - - config.write(pw); - } catch (ConfigurationException | IOException e) { - e.printStackTrace(); - } - return string.toString(); - } - } diff --git a/src/main/java/terrasaur/enums/AltwgDataType.java b/src/main/java/terrasaur/enums/AltwgDataType.java index 8679403..4b1fe9b 100644 --- a/src/main/java/terrasaur/enums/AltwgDataType.java +++ b/src/main/java/terrasaur/enums/AltwgDataType.java @@ -27,1721 +27,1727 @@ import java.util.EnumSet; /** * Contains the enumerations for the different altwg output product types. Used in ALTWG naming * conventions and to determine process flow. - * + * * @author espirrc1 * */ public enum AltwgDataType { - /* - * Each enumeration has the following properties in the order shown: description - general - * description of the product type fileFrag - string fragment to use in ALTWG file naming - * convention units - units of the data values headerValue - string identifying the data value - * comment - comment string (optional) used to fill comment section of fits header anciColName - - * string to use when defining the column name in the ancillary fits file. If null then - * getAnciColName() returns the 'description' string - */ - - DSK("SPICE DSK", "dsk", null, "DSK", "SPICE Shape model format", "DSK") { - @Override - // no distinction between global and local in ICD. - public int getGlobalSpocID() { - return 33; - } - - @Override - // no distinction between global and local in ICD. - public int getLocalSpocID() { - return 34; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - }, - - // NFT plane relative DTM with only subset of planes (excludes position data) - NFTDTM("NFT Plane-relative DTM", "nftdtm", null, "NFTDTM", null, null) { - - @Override - // no distinction between global and local in ICD. - public int getGlobalSpocID() { - return 18; - } - - @Override - // no distinction between global and local in ICD. - public int getLocalSpocID() { - return 18; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - /* - * DTM fits. Includes fits files at various intermediate stages such as the initial fits file from - * Maplet2Fits or CreateMapola, the fits files that contain gravity planes output from - * DistributedGravity, and the fits files with all planes including gravity and tilts (such as - * output from Shape2Tilt in ALTWG pipeline). - */ - DTM("Non-NFT DTM", "dtm", null, "DTM", null, null) { - - @Override - // value is for SPC data source. For OLA data source add 1. - public int getGlobalSpocID() { - return 1; - } - - @Override - // value is for SPC data source. For OLA data source add 1. - public int getLocalSpocID() { - return 3; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // OBJ shape model as FITS file - OBJ("OBJ shape model as FITS", "obj", null, "OBJ", null, null) { - @Override - // no distinction between OLA or SPC data source. - public int getGlobalSpocID() { - return 5; - } - - @Override - // no distinction between OLA or SPC data source. - public int getLocalSpocID() { - return 23; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // SIGMA. Used in calculation of some of the ALTWG products. - SIGMA("Sigma", "unk", "km", "Sigma", null, "Sigma") { - @Override - public int getGlobalSpocID() { - return 47; - } - - @Override - public int getLocalSpocID() { - return 48; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - VERTEX_ERROR("Sigma of vertex vector", "vrt", "km", "Sigma", null, "Vertex Radius") { - - @Override - public int getGlobalSpocID() { - return 47; - } - - @Override - public int getLocalSpocID() { - return 48; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - - }, - - /* - * Note: the header values and and comments may already be duplicated in the PlaneInfo - * enumeration. Changes to the values and comments therefore will have to be done here and in - * PlaneInfo. - * ******************************************************************************************** - * CRITICAL: For Ancillary fits files the headerValue MUST match the keyValue in PlaneInfo. This - * is because the Ancillary Fits code is parsing on the PlaneInfo keyvalue in the Fits header to - * determine what AltwgProductType the plane corresponds to. Will fix this later to be less sloppy - * and less prone to user error. - * ******************************************************************************************** - */ - NORMAL_VECTOR_X("normal vector", "nvf", null, "Normal vector X", null, null) { - @Override - public int getGlobalSpocID() { - return 8; - } - - @Override - public int getLocalSpocID() { - return 24; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.NORM_VECTOR_X; - } - - @Override - public AltwgDataType getSigmaType() { - return AltwgDataType.NORMALX_UNCERTAINTY; - } - - }, - - /* - * make the fileFrag non-compliant with the ALTWG naming convention and different from the - * x-component fileFrag. This way we can tell if the code is trying to write this component to a - * separate file -> IT NEVER SHOULD! - */ - NORMAL_VECTOR_Y("normal vector", "nv2", null, "Normal vector Y", null, null) { - @Override - public int getGlobalSpocID() { - return 8; - } - - @Override - public int getLocalSpocID() { - return 24; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.NORM_VECTOR_Y; - } - - @Override - public AltwgDataType getSigmaType() { - return NORMALY_UNCERTAINTY; - } - - }, - - /* - * make the fileFrag non-compliant with the ALTWG naming convention and different from the - * x-component fileFrag. This way we can tell if the code is trying to write this component to a - * separate file -> IT NEVER SHOULD! - */ - NORMAL_VECTOR_Z("normal vector", "nv3", null, "Normal vector Z", null, null) { - @Override - public int getGlobalSpocID() { - return 8; - } - - @Override - public int getLocalSpocID() { - return 24; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.NORM_VECTOR_Z; - } - - @Override - public AltwgDataType getSigmaType() { - return NORMALZ_UNCERTAINTY; - } - - }, - - GRAVITY_VECTOR_X("gravity vector", "grv", "m/s^2", "Gravity vector X", null, null) { - @Override - public int getGlobalSpocID() { - return 9; - } - - @Override - public int getLocalSpocID() { - return 25; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.GRAV_VECTOR_X; - } - - @Override - public AltwgDataType getSigmaType() { - return GRAVITY_VECTOR_X_UNCERTAINTY; - } - - }, - - /* - * make the fileFrag non-compliant with the ALTWG naming convention and different from the - * x-component fileFrag. This way we can tell if the code is trying to write this component to a - * separate file -> IT NEVER SHOULD! - */ - GRAVITY_VECTOR_Y("gravity vector", "gr2", "m/s^2", "Gravity vector Y", null, null) { - @Override - public int getGlobalSpocID() { - return 9; - } - - @Override - public int getLocalSpocID() { - return 25; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.GRAV_VECTOR_Y; - } - - @Override - public AltwgDataType getSigmaType() { - return AltwgDataType.GRAVITY_VECTOR_Y_UNCERTAINTY; - } - - }, - - /* - * make the fileFrag non-compliant with the ALTWG naming convention and different from the - * x-component fileFrag. This way we can tell if the code is trying to write this component to a - * separate file -> IT NEVER SHOULD! - */ - GRAVITY_VECTOR_Z("gravity vector", "gr3", "m/s^2", "Gravity vector Z", null, null) { - @Override - public int getGlobalSpocID() { - return 9; - } - - @Override - public int getLocalSpocID() { - return 25; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.GRAV_VECTOR_Z; - } - - @Override - public AltwgDataType getSigmaType() { - return GRAVITY_VECTOR_Z_UNCERTAINTY; - } - - }, - - GRAVITATIONAL_MAGNITUDE("gravitational magnitude", "grm", "m/s^2", "Gravitational magnitude", - null, null) { - @Override - public int getGlobalSpocID() { - return 10; - } - - @Override - public int getLocalSpocID() { - return 26; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.GRAV_MAG; - } - - @Override - public AltwgDataType getSigmaType() { - return AltwgDataType.GRAVITATIONAL_MAGNITUDE_UNCERTAINTY; - } - - }, - - GRAVITATIONAL_POTENTIAL("gravitational potential", "pot", "J/kg", "Gravitational potential", null, - null) { - @Override - public int getGlobalSpocID() { - return 11; - } - - @Override - public int getLocalSpocID() { - return 27; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.GRAV_POT; - } - - @Override - public AltwgDataType getSigmaType() { - return AltwgDataType.GRAVITATIONAL_POTENTIAL_UNCERTAINTY; - } - - }, - - ELEVATION("elevation", "elv", "meters", "Elevation", null, null) { - @Override - public int getGlobalSpocID() { - return 12; - } - - @Override - public int getLocalSpocID() { - return 28; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.ELEV; - } - - @Override - public AltwgDataType getSigmaType() { - return EL_UNCERTAINTY; - } - - }, - - // No longer needed! This is same as HEIGHT plane! - // ELEV_NORM("elevation relative to normal plane", "elv", "meters", "Elevation relative to normal - // plane", null, null) { - // @Override - // public int getGlobalSpocID() { - // return -1; - // } - // - // @Override - // public int getLocalSpocID() { - // return -1; - // } - // - // public PlaneInfo getPlaneInfo() { - // return PlaneInfo.ELEV_NORM; - // } - // - // }, - - SLOPE("slope", "slp", "degrees", "Slope", null, null) { - @Override - public int getGlobalSpocID() { - return 13; - } - - @Override - public int getLocalSpocID() { - return 29; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.SLOPE; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_UNCERTAINTY; - } - - }, - - FACET_MAP("Facet Map", "ffi", null, "Facet Coarse Index", null, "Facet Coarse Index") { - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - FACET_TILT("Facet Tilt", "fti", "degrees", "Facet tilt", null, "Facet Tilt") { - @Override - public int getGlobalSpocID() { - return 14; - } - - @Override - public int getLocalSpocID() { - return 30; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_UNCERTAINTY; - } - - }, - - FACET_TILT_DIRECTION("Facet Tilt Direction", "fdi", "degrees", "Facet tilt direction", null, - "Facet Tilt Direction") { - @Override - public int getGlobalSpocID() { - return 35; - } - - @Override - public int getLocalSpocID() { - return 36; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_DIRECTION; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_UNCERTAINTY; - } - - }, - - TILT_AVG("Mean Tilt", "mti", "degrees", "Mean tilt", null, "Mean Tilt") { - @Override - public int getGlobalSpocID() { - return 15; - } - - @Override - public int getLocalSpocID() { - return 31; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_AVERAGE; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_AVG_UNCERTAINTY; - } - - - }, - - TILT_VARIATION("Tilt Variation", "tiv", "degrees", "Tilt variation", null, "Tilt Variation") { - @Override - public int getGlobalSpocID() { - return 16; - } - - @Override - public int getLocalSpocID() { - return 32; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_VARIATION; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_RELATIVE_UNCERTAINTY; - } - - }, - - TILT_AVG_DIRECTION("Mean Tilt Direction", "mdi", "degrees", "Mean tilt direction", null, - "Mean Tilt Direction") { - @Override - public int getGlobalSpocID() { - return 37; - } - - @Override - public int getLocalSpocID() { - return 38; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_AVERAGE_DIRECTION; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_AVG_DIRECTION_UNCERTAINTY; - } - - }, - - TILT_DIRECTION_VARIATION("Tilt Direction Variation", "div", "degrees", "Tilt direction variation", - null, "Tilt Direction Variation") { - @Override - public int getGlobalSpocID() { - return 39; - } - - @Override - public int getLocalSpocID() { - return 40; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_DIRECTION_VARIATION; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_RELATIVE_DIRECTION_UNCERTAINTY; - } - - }, - - TILT_RELATIVE("Relative Tilt", "rti", "degrees", "Relative tilt", null, "Relative Tilt") { - @Override - public int getGlobalSpocID() { - return 41; - } - - @Override - public int getLocalSpocID() { - return 42; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_RELATIVE; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_RELATIVE_UNCERTAINTY; - } - - }, - - TILT_RELATIVE_DIRECTION("Relative Tilt Direction", "rdi", "degrees", "Relative tilt direction", - null, "Relative Tilt Direction") { - @Override - public int getGlobalSpocID() { - return 43; - } - - @Override - public int getLocalSpocID() { - return 44; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.TILT_RELATIVE_DIRECTION; - } - - @Override - public AltwgDataType getSigmaType() { - return TILT_RELATIVE_DIRECTION_UNCERTAINTY; - } - - }, - - /* - * Each enumeration has the following properties in the order shown: description - general - * description of the product type fileFrag - string fragment to use in ALTWG file naming - * convention units - units of the data values headerValue - string identifying the data value - * comment - comment string (optional) used to fill comment section of fits header anciColName - - * string to use when defining the column name in the ancillary fits file. If null then - * getAnciColName() returns the 'description' string - */ - - RELATIVE_HEIGHT("Max height/depth within an ellipse", "mht", "km", "Max relative height", null, - "Max Relative Height") { - @Override - public int getGlobalSpocID() { - return 49; - } - - @Override - public int getLocalSpocID() { - return 50; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.RELATIVE_HEIGHT; - } - - @Override - public AltwgDataType getSigmaType() { - return RELATIVE_HEIGHT_UNCERTAINTY; - } - - }, - - FACETRAD("Facet Radius", "rad", "km", "Facet radius", null, "Facet_Center_Radius") { - @Override - public int getGlobalSpocID() { - return 45; - } - - @Override - public int getLocalSpocID() { - return 46; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.FACETRAD; - } - - @Override - public AltwgDataType getSigmaType() { - return FACETRAD_UNCERTAINTY; - } - - }, - - /* - * ancillary template, scalar order of attributes is: AltwgDataType(String description, String - * fileFrag, String units, String headerValue, String comment, String anciColName) - */ - - TPLATEANCI("Ancillary Template Scalar", "tes", "TBDCOLUNITS", "AncillaryTemplate", null, - "TBDCOLNAME") { - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // ancillary template, scalar - TPLATEANCIVEC("Ancillary Template Vector", "tev", "TBD", "AncillaryTemplate", null, - "TBDCOLNAME") { - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - AREA("Facet Area", "are", "km^2", "Facet Area", null, "Facet Area") { - - @Override - public int getGlobalSpocID() { - return 52; - } - - @Override - public int getLocalSpocID() { - return 53; - } - - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.AREA; - } - - @Override - public AltwgDataType getSigmaType() { - return AREA_UNCERTAINTY; - } - - }, - - /* - * Each enumeration has the following properties in the order shown: description - general - * description of the product type fileFrag - string fragment to use in ALTWG file naming - * convention units - units of the data values headerValue - string identifying the data value - - * should always be filled in! comment - comment string (optional) used to fill comment section of - * fits header anciColName - string to use when defining the column name in the ancillary fits - * file. If null then getAnciColName() returns the 'description' string - */ - AREA_UNCERTAINTY("FacetArea Uncertainty", "notapplicable", "km^2", "facet area uncertainty", null, - "facet area uncertainty") { - - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - EL_UNCERTAINTY("Elevation Uncertainty", "notapplicable", "m", "El uncertainty", null, - "elevation uncertainty") { - - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - /* - * Each enumeration has the following properties in the order shown: description - general - * description of the product type fileFrag - string fragment to use in ALTWG file naming - * convention units - units of the data values headerValue - string identifying the data value - - * should always be filled in! comment - comment string (optional) used to fill comment section of - * fits header anciColName - string to use when defining the column name in the ancillary fits - * file. If null then getAnciColName() returns the 'description' string - */ - - // Uncertainty in gravity vector, x component - GRAVITY_VECTOR_X_UNCERTAINTY("Grav_X Uncertainty", "notapplicable", "m/s^2", "GravX uncertainty", - null, "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in gravity vector, y component - GRAVITY_VECTOR_Y_UNCERTAINTY("Grav_Y Uncertainty", "notapplicable", "m/s^2", "GravY uncertainty", - null, "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in gravity vector, z component - GRAVITY_VECTOR_Z_UNCERTAINTY("Grav_Z Uncertainty", "notapplicable", "m/s^2", "GravZ uncertainty", - null, "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in gravitational magnitude - GRAVITATIONAL_MAGNITUDE_UNCERTAINTY("Grav Mag Uncertainty", "notapplicable", "m/s^2", - "GravMag uncertainty", null, "grav magnitude uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in gravitational potential - GRAVITATIONAL_POTENTIAL_UNCERTAINTY("Grav Pot Uncertainty", "notapplicable", "m/s^2", - "GravPot Uncertainty", null, "grav potential uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - // Uncertainty in Normal Vector, X component - NORMALX_UNCERTAINTY("Normal X Uncertainty", "notapplicable", null, "Normal X Uncertainty", null, - "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in Normal Vector, Y component - NORMALY_UNCERTAINTY("Normal Y Uncertainty", "notapplicable", null, "Normal Y Uncertainty", null, - "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // Uncertainty in Normal Vector, Z component - NORMALZ_UNCERTAINTY("Normal Z Uncertainty", "notapplicable", null, "Normal Z Uncertainty", null, - "vector component uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // tilt uncertainty - /* - * description - general description of the product type fileFrag - string fragment to use in - * ALTWG file naming convention units - units of the data values headerValue - string identifying - * the data value comment - comment string (optional) used to fill comment section of fits header - * anciColName - string to use when defining the column name in the ancillary fits file. If null - * then getAnciColName() returns the 'description' string - */ - TILT_UNCERTAINTY("Tilt Uncertainty", "notapplicable", "degrees", "Tilt Uncertainty", null, - "tilt uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - /* - * tilt variation standard error. Used to populate the SIGMA column for the tilt variation - * ancillary product. - */ - TILTVAR_UNCERTAINTY("Tilt Variation Uncertainty", "notapplicable", "degrees", - "Tilt Variation Uncertainty", null, "variation uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - /* - * tilt variation standard error. Used to populate the SIGMA column for the tilt variation - * ancillary product. - */ - TILTDIRVAR_UNCERTAINTY("Tilt Direction Variation Uncertainty", "notapplicable", "degrees", - "Tilt Direction Variation Uncertainty", null, "direction variation uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - /* - * albedo from SPC description - general description of the product type fileFrag - string - * fragment to use in ALTWG file naming convention units - units of the data values headerValue - - * string identifying the data value comment - comment string (optional) used to fill comment - * section of fits header anciColName - string to use when defining the column name in the - * ancillary fits file. If null then getAnciColName() returns the 'description' string - */ - ALBEDO("Relative albedo", "alb", "unitless", "Relative albedo", null, "relative albedo") { - - @Override - public int getGlobalSpocID() { - return 55; - } - - @Override - public int getLocalSpocID() { - return 54; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.ALBEDO; - } - - @Override - public AltwgDataType getSigmaType() { - return ALBEDO_UNCERTAINTY; - } - - }, - - ALBEDO_UNCERTAINTY("Albedo Uncertainty", "notapplicable", "unitless", "Albedo uncertainty", null, - "albedo uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // OLA intensity. Used in place of albedo - INTENSITY("Intensity", "alb", "unitless", "Intensity", null, "intensity") { - - @Override - public int getGlobalSpocID() { - return 55; - } - - @Override - public int getLocalSpocID() { - return 54; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return PlaneInfo.INTENSITY; - } - - @Override - public AltwgDataType getSigmaType() { - return INTENSITY_UNCERTAINTY; - } - - }, - - - // OLA intensity. Used in place of albedo - INTENSITY_UNCERTAINTY("Intensity", "alb", "unitless", "Intensity", null, "intensity") { - - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - /* - * Each enumeration has the following properties in the order shown: description - general - * description of the product type fileFrag - string fragment to use in ALTWG file naming - * convention units - units of the data values headerValue - string identifying the data value - * comment - comment string (optional) used to fill comment section of fits header anciColName - - * string to use when defining the column name in the ancillary fits file. If null then - * getAnciColName() returns the 'description' string - */ - - RELATIVE_HEIGHT_UNCERTAINTY("Max relative height uncertainty", "notapplicable", "km", - "Max relative height uncertainty", null, "max relative height uncertainty") { - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // facet radius uncertainty - FACETRAD_UNCERTAINTY("Facet Radius Uncertainty", "notapplicable", null, - "Facet Radius Uncertainty", null, "facet radius uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - // uncertainty in mean tilt (TILT_AVG) - TILT_AVG_UNCERTAINTY("Mean Tilt Uncertainty", "notapplicable", null, "Mean Tilt Uncertainty", - null, "mean tilt uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - // uncertainty in mean tilt direction (TILT_AVG_DIRECTION) - TILT_AVG_DIRECTION_UNCERTAINTY("Mean Tilt Direction Uncertainty", "notapplicable", null, - "Mean Tilt Direction Uncertainty", null, "mean tilt direction uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - // uncertainty in relative tilt (TILT_RELATIVE) - TILT_RELATIVE_UNCERTAINTY("Relative Tilt Uncertainty", "notapplicable", null, - "Relative Tilt Uncertainty", null, "relative tilt uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - - // uncertainty in relative tilt direction (TILT_RELATIVE_DIRECTION) - TILT_RELATIVE_DIRECTION_UNCERTAINTY("Relative Tilt Direction Uncertainty", "notapplicable", null, - "Relative Tilt Direction Uncertainty", null, "relative tilt direction uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - // uncertainty in relative tilt direction (TILT_RELATIVE_DIRECTION) - TILT_DIRECTION_UNCERTAINTY("Tilt Direction Uncertainty", "notapplicable", null, - "Tilt Direction Uncertainty", null, "tilt direction uncertainty") { - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getGlobalSpocID() { - return -1; - } - - @Override - // not a SPOC product, just the SIGMA column in the ancillary fits file - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }, - - /* - * not applicable. Usually indicates that one does not need to follow ALTWG naming convention or - * that a valid AltwgDataType could not be returned. DO NOT USE AS A SUBSTITUTE FOR TBD! - */ - NA("not applicable", "unkproducttype", null, "Not applicable", null, null) { - @Override - public int getGlobalSpocID() { - return -1; - } - - @Override - public int getLocalSpocID() { - return -1; - } - - // no such plane in DTM - @Override - public PlaneInfo getPlaneInfo() { - return null; - } - - @Override - public AltwgDataType getSigmaType() { - return NA; - } - - }; - - - private String description; // description of enumeration - private String fileFrag; - private String units; - private String headerValue; // name to use as value in Fits Header - private String comment; // comment to add to fits header (optional) - private String anciColName; - - AltwgDataType(String description, String fileFrag, String units, String headerValue, - String comment, String anciColName) { - this.description = description; - this.fileFrag = fileFrag; - this.units = units; - this.headerValue = headerValue; - this.comment = comment; - this.anciColName = anciColName; - } - - public String getAnciColName() { - if (this.anciColName == null) { - return getDesc(); - } else { - return this.anciColName; - } - } - - public String getDesc() { - return description; - } - - public String getFileFrag() { - return fileFrag; - } - - public String getUnits() { - return units; - } - - public String getHeaderValue() { - return headerValue; - } - - public String getHeaderValueWithUnits() { - if (units == null) - return headerValue; - else - return headerValue + " (" + units + ")"; - } - - public String desc() { - return description; - } - - /** - * Return file fragment w/ "_" delimiters, the way they would look in the altwg filename - * - * @return - */ - public static String getFragDelim(AltwgDataType prodType) { - String delim = "_"; - String frag = delim + prodType.getFileFrag() + delim; - return frag; - } - - /* - * Enumeration set that contains all the gravity ancillary fits product types. - * - * For vectors we only store the enum for the X component in this set since all components (x,y,z) - * will be written together into one data file, so we don't need to do it again for the Y or Z - * AltwgProductTypes. - * - */ - public static final EnumSet anciGravitySet = - EnumSet.of(AltwgDataType.GRAVITY_VECTOR_X, AltwgDataType.GRAVITATIONAL_MAGNITUDE, - AltwgDataType.GRAVITATIONAL_POTENTIAL, AltwgDataType.ELEVATION); - - // slope is no longer in anciGravitySet, because it depends on errors that are determined - // in Shape2Tilt. See MultiTableToAncillaryFits.java - // AltwgDataType.SLOPE); - - /* - * Enumeration set that contains all the ancillary fits types. These are product types only - * associated w/ ancillary fits files. - * - */ - public static final EnumSet ancillaryTypes = - EnumSet.range(AltwgDataType.VERTEX_ERROR, AltwgDataType.FACETRAD); - - public static final EnumSet simpleAnciProducts = - EnumSet.range(AltwgDataType.NORMAL_VECTOR_X, AltwgDataType.FACETRAD); - - /* - * Enumeration set that contains only the planes which are written out by the - * DistributedGravity.saveResultsAtCenters() option. This used to include tilt values, but we are - * migrating to having DistributedGravity ONLY print out gravity derived planes. Another class - * will take care of adding tilt planes afterwards. This enumset contains all components of the - * gravity vectors because each component is written out to a separate plane. - */ - // public static final EnumSet gravityPlanes = - // EnumSet.range(AltwgDataType.NORMAL_VECTOR_X, - // AltwgDataType.SLOPE); - - public static final EnumSet gravityPlanes = - EnumSet.of(AltwgDataType.NORMAL_VECTOR_X, AltwgDataType.NORMAL_VECTOR_Y, - AltwgDataType.NORMAL_VECTOR_Z, AltwgDataType.GRAVITY_VECTOR_X, - AltwgDataType.GRAVITY_VECTOR_Y, AltwgDataType.GRAVITY_VECTOR_Z, - AltwgDataType.GRAVITATIONAL_MAGNITUDE, AltwgDataType.GRAVITATIONAL_POTENTIAL, - AltwgDataType.ELEVATION, AltwgDataType.SLOPE, AltwgDataType.AREA); - - public static final EnumSet tiltPlanes = - EnumSet.range(AltwgDataType.FACET_TILT, AltwgDataType.RELATIVE_HEIGHT); - - - /** - * Unique identifier for ALTWG global product in OSIRIS-REx SPOC system. - * - * @return - */ - public abstract int getGlobalSpocID(); - - /** - * Unique identifier for ALTWG local product in OSIRIS-REx SPOC system. - * - * @return - */ - public abstract int getLocalSpocID(); - - /** - * Retrieve PlaneInfo associated with this enum. PlaneInfo describes the plane in the DTM that - * contains data pertaining to this enum. If null then there is no associated plane in the DTM. - * - * @return - */ - public abstract PlaneInfo getPlaneInfo(); - - /** - * Get the AltwgDataType that is associated with the sigma values for this type. Only applicable - * if AltwgDataType refers to a set of data values in an ancillary fits file. For example, there - * is no associated sigma for a DTM fits file or an OBJ shape model in this context. - * - * @return - */ - public abstract AltwgDataType getSigmaType(); - - /** - * Return a string describing the sigma associated with the data column in an ancillary fits - * table. Return "NA" if not applicable. - * - * @return - */ - // public abstract String getDataSigmaDef(); + /* + * Each enumeration has the following properties in the order shown: description - general + * description of the product type fileFrag - string fragment to use in ALTWG file naming + * convention units - units of the data values headerValue - string identifying the data value + * comment - comment string (optional) used to fill comment section of fits header anciColName - + * string to use when defining the column name in the ancillary fits file. If null then + * getAnciColName() returns the 'description' string + */ + + DSK("SPICE DSK", "dsk", null, "DSK", "SPICE Shape model format", "DSK") { + @Override + // no distinction between global and local in ICD. + public int getGlobalSpocID() { + return 33; + } + + @Override + // no distinction between global and local in ICD. + public int getLocalSpocID() { + return 34; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // NFT plane relative DTM with only subset of planes (excludes position data) + NFTDTM("NFT Plane-relative DTM", "nftdtm", null, "NFTDTM", null, null) { + + @Override + // no distinction between global and local in ICD. + public int getGlobalSpocID() { + return 18; + } + + @Override + // no distinction between global and local in ICD. + public int getLocalSpocID() { + return 18; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * DTM fits. Includes fits files at various intermediate stages such as the initial fits file from + * Maplet2Fits or CreateMapola, the fits files that contain gravity planes output from + * DistributedGravity, and the fits files with all planes including gravity and tilts (such as + * output from Shape2Tilt in ALTWG pipeline). + */ + DTM("Non-NFT DTM", "dtm", null, "DTM", null, null) { + + @Override + // value is for SPC data source. For OLA data source add 1. + public int getGlobalSpocID() { + return 1; + } + + @Override + // value is for SPC data source. For OLA data source add 1. + public int getLocalSpocID() { + return 3; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // OBJ shape model as FITS file + OBJ("OBJ shape model as FITS", "obj", null, "OBJ", null, null) { + @Override + // no distinction between OLA or SPC data source. + public int getGlobalSpocID() { + return 5; + } + + @Override + // no distinction between OLA or SPC data source. + public int getLocalSpocID() { + return 23; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // SIGMA. Used in calculation of some of the ALTWG products. + SIGMA("Sigma", "unk", "km", "Sigma", null, "Sigma") { + @Override + public int getGlobalSpocID() { + return 47; + } + + @Override + public int getLocalSpocID() { + return 48; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + VERTEX_ERROR("Sigma of vertex vector", "vrt", "km", "Sigma", null, "Vertex Radius") { + + @Override + public int getGlobalSpocID() { + return 47; + } + + @Override + public int getLocalSpocID() { + return 48; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * Note: the header values and and comments may already be duplicated in the PlaneInfo + * enumeration. Changes to the values and comments therefore will have to be done here and in + * PlaneInfo. + * ******************************************************************************************** + * CRITICAL: For Ancillary fits files the headerValue MUST match the keyValue in PlaneInfo. This + * is because the Ancillary Fits code is parsing on the PlaneInfo keyvalue in the Fits header to + * determine what AltwgProductType the plane corresponds to. Will fix this later to be less sloppy + * and less prone to user error. + * ******************************************************************************************** + */ + NORMAL_VECTOR_X("normal vector", "nvf", null, "Normal vector X", null, null) { + @Override + public int getGlobalSpocID() { + return 8; + } + + @Override + public int getLocalSpocID() { + return 24; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.NORM_VECTOR_X; + } + + @Override + public AltwgDataType getSigmaType() { + return AltwgDataType.NORMALX_UNCERTAINTY; + } + }, + + /* + * make the fileFrag non-compliant with the ALTWG naming convention and different from the + * x-component fileFrag. This way we can tell if the code is trying to write this component to a + * separate file -> IT NEVER SHOULD! + */ + NORMAL_VECTOR_Y("normal vector", "nv2", null, "Normal vector Y", null, null) { + @Override + public int getGlobalSpocID() { + return 8; + } + + @Override + public int getLocalSpocID() { + return 24; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.NORM_VECTOR_Y; + } + + @Override + public AltwgDataType getSigmaType() { + return NORMALY_UNCERTAINTY; + } + }, + + /* + * make the fileFrag non-compliant with the ALTWG naming convention and different from the + * x-component fileFrag. This way we can tell if the code is trying to write this component to a + * separate file -> IT NEVER SHOULD! + */ + NORMAL_VECTOR_Z("normal vector", "nv3", null, "Normal vector Z", null, null) { + @Override + public int getGlobalSpocID() { + return 8; + } + + @Override + public int getLocalSpocID() { + return 24; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.NORM_VECTOR_Z; + } + + @Override + public AltwgDataType getSigmaType() { + return NORMALZ_UNCERTAINTY; + } + }, + + GRAVITY_VECTOR_X("gravity vector", "grv", "m/s^2", "Gravity vector X", null, null) { + @Override + public int getGlobalSpocID() { + return 9; + } + + @Override + public int getLocalSpocID() { + return 25; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.GRAV_VECTOR_X; + } + + @Override + public AltwgDataType getSigmaType() { + return GRAVITY_VECTOR_X_UNCERTAINTY; + } + }, + + /* + * make the fileFrag non-compliant with the ALTWG naming convention and different from the + * x-component fileFrag. This way we can tell if the code is trying to write this component to a + * separate file -> IT NEVER SHOULD! + */ + GRAVITY_VECTOR_Y("gravity vector", "gr2", "m/s^2", "Gravity vector Y", null, null) { + @Override + public int getGlobalSpocID() { + return 9; + } + + @Override + public int getLocalSpocID() { + return 25; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.GRAV_VECTOR_Y; + } + + @Override + public AltwgDataType getSigmaType() { + return AltwgDataType.GRAVITY_VECTOR_Y_UNCERTAINTY; + } + }, + + /* + * make the fileFrag non-compliant with the ALTWG naming convention and different from the + * x-component fileFrag. This way we can tell if the code is trying to write this component to a + * separate file -> IT NEVER SHOULD! + */ + GRAVITY_VECTOR_Z("gravity vector", "gr3", "m/s^2", "Gravity vector Z", null, null) { + @Override + public int getGlobalSpocID() { + return 9; + } + + @Override + public int getLocalSpocID() { + return 25; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.GRAV_VECTOR_Z; + } + + @Override + public AltwgDataType getSigmaType() { + return GRAVITY_VECTOR_Z_UNCERTAINTY; + } + }, + + GRAVITATIONAL_MAGNITUDE("gravitational magnitude", "grm", "m/s^2", "Gravitational magnitude", null, null) { + @Override + public int getGlobalSpocID() { + return 10; + } + + @Override + public int getLocalSpocID() { + return 26; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.GRAV_MAG; + } + + @Override + public AltwgDataType getSigmaType() { + return AltwgDataType.GRAVITATIONAL_MAGNITUDE_UNCERTAINTY; + } + }, + + GRAVITATIONAL_POTENTIAL("gravitational potential", "pot", "J/kg", "Gravitational potential", null, null) { + @Override + public int getGlobalSpocID() { + return 11; + } + + @Override + public int getLocalSpocID() { + return 27; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.GRAV_POT; + } + + @Override + public AltwgDataType getSigmaType() { + return AltwgDataType.GRAVITATIONAL_POTENTIAL_UNCERTAINTY; + } + }, + + ELEVATION("elevation", "elv", "meters", "Elevation", null, null) { + @Override + public int getGlobalSpocID() { + return 12; + } + + @Override + public int getLocalSpocID() { + return 28; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.ELEV; + } + + @Override + public AltwgDataType getSigmaType() { + return EL_UNCERTAINTY; + } + }, + + // No longer needed! This is same as HEIGHT plane! + // ELEV_NORM("elevation relative to normal plane", "elv", "meters", "Elevation relative to normal + // plane", null, null) { + // @Override + // public int getGlobalSpocID() { + // return -1; + // } + // + // @Override + // public int getLocalSpocID() { + // return -1; + // } + // + // public PlaneInfo getPlaneInfo() { + // return PlaneInfo.ELEV_NORM; + // } + // + // }, + + SLOPE("slope", "slp", "degrees", "Slope", null, null) { + @Override + public int getGlobalSpocID() { + return 13; + } + + @Override + public int getLocalSpocID() { + return 29; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.SLOPE; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_UNCERTAINTY; + } + }, + + FACET_MAP("Facet Map", "ffi", null, "Facet Coarse Index", null, "Facet Coarse Index") { + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + FACET_TILT("Facet Tilt", "fti", "degrees", "Facet tilt", null, "Facet Tilt") { + @Override + public int getGlobalSpocID() { + return 14; + } + + @Override + public int getLocalSpocID() { + return 30; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_UNCERTAINTY; + } + }, + + FACET_TILT_DIRECTION( + "Facet Tilt Direction", "fdi", "degrees", "Facet tilt direction", null, "Facet Tilt Direction") { + @Override + public int getGlobalSpocID() { + return 35; + } + + @Override + public int getLocalSpocID() { + return 36; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_DIRECTION; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_UNCERTAINTY; + } + }, + + TILT_AVG("Mean Tilt", "mti", "degrees", "Mean tilt", null, "Mean Tilt") { + @Override + public int getGlobalSpocID() { + return 15; + } + + @Override + public int getLocalSpocID() { + return 31; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_AVERAGE; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_AVG_UNCERTAINTY; + } + }, + + TILT_VARIATION("Tilt Variation", "tiv", "degrees", "Tilt variation", null, "Tilt Variation") { + @Override + public int getGlobalSpocID() { + return 16; + } + + @Override + public int getLocalSpocID() { + return 32; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_VARIATION; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_RELATIVE_UNCERTAINTY; + } + }, + + TILT_AVG_DIRECTION("Mean Tilt Direction", "mdi", "degrees", "Mean tilt direction", null, "Mean Tilt Direction") { + @Override + public int getGlobalSpocID() { + return 37; + } + + @Override + public int getLocalSpocID() { + return 38; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_AVERAGE_DIRECTION; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_AVG_DIRECTION_UNCERTAINTY; + } + }, + + TILT_DIRECTION_VARIATION( + "Tilt Direction Variation", + "div", + "degrees", + "Tilt direction variation", + null, + "Tilt Direction Variation") { + @Override + public int getGlobalSpocID() { + return 39; + } + + @Override + public int getLocalSpocID() { + return 40; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_DIRECTION_VARIATION; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_RELATIVE_DIRECTION_UNCERTAINTY; + } + }, + + TILT_RELATIVE("Relative Tilt", "rti", "degrees", "Relative tilt", null, "Relative Tilt") { + @Override + public int getGlobalSpocID() { + return 41; + } + + @Override + public int getLocalSpocID() { + return 42; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_RELATIVE; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_RELATIVE_UNCERTAINTY; + } + }, + + TILT_RELATIVE_DIRECTION( + "Relative Tilt Direction", "rdi", "degrees", "Relative tilt direction", null, "Relative Tilt Direction") { + @Override + public int getGlobalSpocID() { + return 43; + } + + @Override + public int getLocalSpocID() { + return 44; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.TILT_RELATIVE_DIRECTION; + } + + @Override + public AltwgDataType getSigmaType() { + return TILT_RELATIVE_DIRECTION_UNCERTAINTY; + } + }, + + /* + * Each enumeration has the following properties in the order shown: description - general + * description of the product type fileFrag - string fragment to use in ALTWG file naming + * convention units - units of the data values headerValue - string identifying the data value + * comment - comment string (optional) used to fill comment section of fits header anciColName - + * string to use when defining the column name in the ancillary fits file. If null then + * getAnciColName() returns the 'description' string + */ + + RELATIVE_HEIGHT( + "Max height/depth within an ellipse", "mht", "km", "Max relative height", null, "Max Relative Height") { + @Override + public int getGlobalSpocID() { + return 49; + } + + @Override + public int getLocalSpocID() { + return 50; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.RELATIVE_HEIGHT; + } + + @Override + public AltwgDataType getSigmaType() { + return RELATIVE_HEIGHT_UNCERTAINTY; + } + }, + + FACETRAD("Facet Radius", "rad", "km", "Facet radius", null, "Facet_Center_Radius") { + @Override + public int getGlobalSpocID() { + return 45; + } + + @Override + public int getLocalSpocID() { + return 46; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.FACETRAD; + } + + @Override + public AltwgDataType getSigmaType() { + return FACETRAD_UNCERTAINTY; + } + }, + + /* + * ancillary template, scalar order of attributes is: AltwgDataType(String description, String + * fileFrag, String units, String headerValue, String comment, String anciColName) + */ + + TPLATEANCI("Ancillary Template Scalar", "tes", "TBDCOLUNITS", "AncillaryTemplate", null, "TBDCOLNAME") { + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // ancillary template, scalar + TPLATEANCIVEC("Ancillary Template Vector", "tev", "TBD", "AncillaryTemplate", null, "TBDCOLNAME") { + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + AREA("Facet Area", "are", "km^2", "Facet Area", null, "Facet Area") { + + @Override + public int getGlobalSpocID() { + return 52; + } + + @Override + public int getLocalSpocID() { + return 53; + } + + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.AREA; + } + + @Override + public AltwgDataType getSigmaType() { + return AREA_UNCERTAINTY; + } + }, + + /* + * Each enumeration has the following properties in the order shown: description - general + * description of the product type fileFrag - string fragment to use in ALTWG file naming + * convention units - units of the data values headerValue - string identifying the data value - + * should always be filled in! comment - comment string (optional) used to fill comment section of + * fits header anciColName - string to use when defining the column name in the ancillary fits + * file. If null then getAnciColName() returns the 'description' string + */ + AREA_UNCERTAINTY( + "FacetArea Uncertainty", + "notapplicable", + "km^2", + "facet area uncertainty", + null, + "facet area uncertainty") { + + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + EL_UNCERTAINTY("Elevation Uncertainty", "notapplicable", "m", "El uncertainty", null, "elevation uncertainty") { + + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * Each enumeration has the following properties in the order shown: description - general + * description of the product type fileFrag - string fragment to use in ALTWG file naming + * convention units - units of the data values headerValue - string identifying the data value - + * should always be filled in! comment - comment string (optional) used to fill comment section of + * fits header anciColName - string to use when defining the column name in the ancillary fits + * file. If null then getAnciColName() returns the 'description' string + */ + + // Uncertainty in gravity vector, x component + GRAVITY_VECTOR_X_UNCERTAINTY( + "Grav_X Uncertainty", "notapplicable", "m/s^2", "GravX uncertainty", null, "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in gravity vector, y component + GRAVITY_VECTOR_Y_UNCERTAINTY( + "Grav_Y Uncertainty", "notapplicable", "m/s^2", "GravY uncertainty", null, "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in gravity vector, z component + GRAVITY_VECTOR_Z_UNCERTAINTY( + "Grav_Z Uncertainty", "notapplicable", "m/s^2", "GravZ uncertainty", null, "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in gravitational magnitude + GRAVITATIONAL_MAGNITUDE_UNCERTAINTY( + "Grav Mag Uncertainty", + "notapplicable", + "m/s^2", + "GravMag uncertainty", + null, + "grav magnitude uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in gravitational potential + GRAVITATIONAL_POTENTIAL_UNCERTAINTY( + "Grav Pot Uncertainty", + "notapplicable", + "m/s^2", + "GravPot Uncertainty", + null, + "grav potential uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in Normal Vector, X component + NORMALX_UNCERTAINTY( + "Normal X Uncertainty", + "notapplicable", + null, + "Normal X Uncertainty", + null, + "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in Normal Vector, Y component + NORMALY_UNCERTAINTY( + "Normal Y Uncertainty", + "notapplicable", + null, + "Normal Y Uncertainty", + null, + "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // Uncertainty in Normal Vector, Z component + NORMALZ_UNCERTAINTY( + "Normal Z Uncertainty", + "notapplicable", + null, + "Normal Z Uncertainty", + null, + "vector component uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // tilt uncertainty + /* + * description - general description of the product type fileFrag - string fragment to use in + * ALTWG file naming convention units - units of the data values headerValue - string identifying + * the data value comment - comment string (optional) used to fill comment section of fits header + * anciColName - string to use when defining the column name in the ancillary fits file. If null + * then getAnciColName() returns the 'description' string + */ + TILT_UNCERTAINTY("Tilt Uncertainty", "notapplicable", "degrees", "Tilt Uncertainty", null, "tilt uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * tilt variation standard error. Used to populate the SIGMA column for the tilt variation + * ancillary product. + */ + TILTVAR_UNCERTAINTY( + "Tilt Variation Uncertainty", + "notapplicable", + "degrees", + "Tilt Variation Uncertainty", + null, + "variation uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * tilt variation standard error. Used to populate the SIGMA column for the tilt variation + * ancillary product. + */ + TILTDIRVAR_UNCERTAINTY( + "Tilt Direction Variation Uncertainty", + "notapplicable", + "degrees", + "Tilt Direction Variation Uncertainty", + null, + "direction variation uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * albedo from SPC description - general description of the product type fileFrag - string + * fragment to use in ALTWG file naming convention units - units of the data values headerValue - + * string identifying the data value comment - comment string (optional) used to fill comment + * section of fits header anciColName - string to use when defining the column name in the + * ancillary fits file. If null then getAnciColName() returns the 'description' string + */ + ALBEDO("Relative albedo", "alb", "unitless", "Relative albedo", null, "relative albedo") { + + @Override + public int getGlobalSpocID() { + return 55; + } + + @Override + public int getLocalSpocID() { + return 54; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.ALBEDO; + } + + @Override + public AltwgDataType getSigmaType() { + return ALBEDO_UNCERTAINTY; + } + }, + + ALBEDO_UNCERTAINTY( + "Albedo Uncertainty", "notapplicable", "unitless", "Albedo uncertainty", null, "albedo uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // OLA intensity. Used in place of albedo + INTENSITY("Intensity", "alb", "unitless", "Intensity", null, "intensity") { + + @Override + public int getGlobalSpocID() { + return 55; + } + + @Override + public int getLocalSpocID() { + return 54; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return PlaneInfo.INTENSITY; + } + + @Override + public AltwgDataType getSigmaType() { + return INTENSITY_UNCERTAINTY; + } + }, + + // OLA intensity. Used in place of albedo + INTENSITY_UNCERTAINTY("Intensity", "alb", "unitless", "Intensity", null, "intensity") { + + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * Each enumeration has the following properties in the order shown: description - general + * description of the product type fileFrag - string fragment to use in ALTWG file naming + * convention units - units of the data values headerValue - string identifying the data value + * comment - comment string (optional) used to fill comment section of fits header anciColName - + * string to use when defining the column name in the ancillary fits file. If null then + * getAnciColName() returns the 'description' string + */ + + RELATIVE_HEIGHT_UNCERTAINTY( + "Max relative height uncertainty", + "notapplicable", + "km", + "Max relative height uncertainty", + null, + "max relative height uncertainty") { + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // facet radius uncertainty + FACETRAD_UNCERTAINTY( + "Facet Radius Uncertainty", + "notapplicable", + null, + "Facet Radius Uncertainty", + null, + "facet radius uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // uncertainty in mean tilt (TILT_AVG) + TILT_AVG_UNCERTAINTY( + "Mean Tilt Uncertainty", "notapplicable", null, "Mean Tilt Uncertainty", null, "mean tilt uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // uncertainty in mean tilt direction (TILT_AVG_DIRECTION) + TILT_AVG_DIRECTION_UNCERTAINTY( + "Mean Tilt Direction Uncertainty", + "notapplicable", + null, + "Mean Tilt Direction Uncertainty", + null, + "mean tilt direction uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // uncertainty in relative tilt (TILT_RELATIVE) + TILT_RELATIVE_UNCERTAINTY( + "Relative Tilt Uncertainty", + "notapplicable", + null, + "Relative Tilt Uncertainty", + null, + "relative tilt uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // uncertainty in relative tilt direction (TILT_RELATIVE_DIRECTION) + TILT_RELATIVE_DIRECTION_UNCERTAINTY( + "Relative Tilt Direction Uncertainty", + "notapplicable", + null, + "Relative Tilt Direction Uncertainty", + null, + "relative tilt direction uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + // uncertainty in relative tilt direction (TILT_RELATIVE_DIRECTION) + TILT_DIRECTION_UNCERTAINTY( + "Tilt Direction Uncertainty", + "notapplicable", + null, + "Tilt Direction Uncertainty", + null, + "tilt direction uncertainty") { + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getGlobalSpocID() { + return -1; + } + + @Override + // not a SPOC product, just the SIGMA column in the ancillary fits file + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }, + + /* + * not applicable. Usually indicates that one does not need to follow ALTWG naming convention or + * that a valid AltwgDataType could not be returned. DO NOT USE AS A SUBSTITUTE FOR TBD! + */ + NA("not applicable", "unkproducttype", null, "Not applicable", null, null) { + @Override + public int getGlobalSpocID() { + return -1; + } + + @Override + public int getLocalSpocID() { + return -1; + } + + // no such plane in DTM + @Override + public PlaneInfo getPlaneInfo() { + return null; + } + + @Override + public AltwgDataType getSigmaType() { + return NA; + } + }; + + private String description; // description of enumeration + private String fileFrag; + private String units; + private String headerValue; // name to use as value in Fits Header + private String comment; // comment to add to fits header (optional) + private String anciColName; + + AltwgDataType( + String description, String fileFrag, String units, String headerValue, String comment, String anciColName) { + this.description = description; + this.fileFrag = fileFrag; + this.units = units; + this.headerValue = headerValue; + this.comment = comment; + this.anciColName = anciColName; + } + + public String getAnciColName() { + if (this.anciColName == null) { + return getDesc(); + } else { + return this.anciColName; + } + } + + public String getDesc() { + return description; + } + + public String getFileFrag() { + return fileFrag; + } + + public String getUnits() { + return units; + } + + public String getHeaderValue() { + return headerValue; + } + + public String getHeaderValueWithUnits() { + if (units == null) return headerValue; + else return headerValue + " (" + units + ")"; + } + + public String desc() { + return description; + } + + /** + * Return file fragment w/ "_" delimiters, the way they would look in the altwg filename + * + * @return + */ + public static String getFragDelim(AltwgDataType prodType) { + String delim = "_"; + String frag = delim + prodType.getFileFrag() + delim; + return frag; + } + + /* + * Enumeration set that contains all the gravity ancillary fits product types. + * + * For vectors we only store the enum for the X component in this set since all components (x,y,z) + * will be written together into one data file, so we don't need to do it again for the Y or Z + * AltwgProductTypes. + * + */ + public static final EnumSet anciGravitySet = EnumSet.of( + AltwgDataType.GRAVITY_VECTOR_X, + AltwgDataType.GRAVITATIONAL_MAGNITUDE, + AltwgDataType.GRAVITATIONAL_POTENTIAL, + AltwgDataType.ELEVATION); + + // slope is no longer in anciGravitySet, because it depends on errors that are determined + // in Shape2Tilt. See MultiTableToAncillaryFits.java + // AltwgDataType.SLOPE); + + /* + * Enumeration set that contains all the ancillary fits types. These are product types only + * associated w/ ancillary fits files. + * + */ + public static final EnumSet ancillaryTypes = + EnumSet.range(AltwgDataType.VERTEX_ERROR, AltwgDataType.FACETRAD); + + public static final EnumSet simpleAnciProducts = + EnumSet.range(AltwgDataType.NORMAL_VECTOR_X, AltwgDataType.FACETRAD); + + /* + * Enumeration set that contains only the planes which are written out by the + * DistributedGravity.saveResultsAtCenters() option. This used to include tilt values, but we are + * migrating to having DistributedGravity ONLY print out gravity derived planes. Another class + * will take care of adding tilt planes afterwards. This enumset contains all components of the + * gravity vectors because each component is written out to a separate plane. + */ + // public static final EnumSet gravityPlanes = + // EnumSet.range(AltwgDataType.NORMAL_VECTOR_X, + // AltwgDataType.SLOPE); + + public static final EnumSet gravityPlanes = EnumSet.of( + AltwgDataType.NORMAL_VECTOR_X, + AltwgDataType.NORMAL_VECTOR_Y, + AltwgDataType.NORMAL_VECTOR_Z, + AltwgDataType.GRAVITY_VECTOR_X, + AltwgDataType.GRAVITY_VECTOR_Y, + AltwgDataType.GRAVITY_VECTOR_Z, + AltwgDataType.GRAVITATIONAL_MAGNITUDE, + AltwgDataType.GRAVITATIONAL_POTENTIAL, + AltwgDataType.ELEVATION, + AltwgDataType.SLOPE, + AltwgDataType.AREA); + + public static final EnumSet tiltPlanes = + EnumSet.range(AltwgDataType.FACET_TILT, AltwgDataType.RELATIVE_HEIGHT); + + /** + * Unique identifier for ALTWG global product in OSIRIS-REx SPOC system. + * + * @return + */ + public abstract int getGlobalSpocID(); + + /** + * Unique identifier for ALTWG local product in OSIRIS-REx SPOC system. + * + * @return + */ + public abstract int getLocalSpocID(); + + /** + * Retrieve PlaneInfo associated with this enum. PlaneInfo describes the plane in the DTM that + * contains data pertaining to this enum. If null then there is no associated plane in the DTM. + * + * @return + */ + public abstract PlaneInfo getPlaneInfo(); + + /** + * Get the AltwgDataType that is associated with the sigma values for this type. Only applicable + * if AltwgDataType refers to a set of data values in an ancillary fits file. For example, there + * is no associated sigma for a DTM fits file or an OBJ shape model in this context. + * + * @return + */ + public abstract AltwgDataType getSigmaType(); + + /** + * Return a string describing the sigma associated with the data column in an ancillary fits + * table. Return "NA" if not applicable. + * + * @return + */ + // public abstract String getDataSigmaDef(); } diff --git a/src/main/java/terrasaur/enums/FORMATS.java b/src/main/java/terrasaur/enums/FORMATS.java index e0ae8a3..5a3d2ef 100644 --- a/src/main/java/terrasaur/enums/FORMATS.java +++ b/src/main/java/terrasaur/enums/FORMATS.java @@ -25,58 +25,64 @@ package terrasaur.enums; import org.apache.commons.io.FilenameUtils; public enum FORMATS { + ASCII(true), + BIN3(true), + BIN4(true), + BIN7(true), + FITS(false), + ICQ(false), + OBJ(false), + PLT(false), + PLY(false), + VTK(false); - ASCII(true), BIN3(true), BIN4(true), BIN7(true), FITS(false), ICQ(false), OBJ(false), PLT( - false), PLY(false), VTK(false); + /** True if this format contains no facet information */ + public boolean pointsOnly; - /** True if this format contains no facet information */ - public boolean pointsOnly; - - private FORMATS(boolean pointsOnly) { - this.pointsOnly = pointsOnly; - } - - /** - * Guess the format from the (case-insensitive) filename extension. - *

- * ASCII: ascii, txt, xyz - *

- * BINARY: binary, bin - *

- * FITS: fits, fit - *

- * L2: l2, dat - *

- * OBJ: obj - *

- * PLT: plt - *

- * PLY: ply - *

- * VTK: vtk - * - * @param filename - * @return matched format type, or null if a match is not found - */ - public static FORMATS formatFromExtension(String filename) { - String extension = FilenameUtils.getExtension(filename); - for (FORMATS f : FORMATS.values()) { - if (f.name().equalsIgnoreCase(extension)) { - return f; - } + private FORMATS(boolean pointsOnly) { + this.pointsOnly = pointsOnly; } - switch (extension.toUpperCase()) { - case "TXT": - case "XYZ": - return FORMATS.ASCII; - case "BIN": - return FORMATS.BIN3; - case "FIT": - return FORMATS.FITS; + /** + * Guess the format from the (case-insensitive) filename extension. + *

+ * ASCII: ascii, txt, xyz + *

+ * BINARY: binary, bin + *

+ * FITS: fits, fit + *

+ * L2: l2, dat + *

+ * OBJ: obj + *

+ * PLT: plt + *

+ * PLY: ply + *

+ * VTK: vtk + * + * @param filename + * @return matched format type, or null if a match is not found + */ + public static FORMATS formatFromExtension(String filename) { + String extension = FilenameUtils.getExtension(filename); + for (FORMATS f : FORMATS.values()) { + if (f.name().equalsIgnoreCase(extension)) { + return f; + } + } + + switch (extension.toUpperCase()) { + case "TXT": + case "XYZ": + return FORMATS.ASCII; + case "BIN": + return FORMATS.BIN3; + case "FIT": + return FORMATS.FITS; + } + + return null; } - - return null; - } - } diff --git a/src/main/java/terrasaur/enums/FitsHeaderType.java b/src/main/java/terrasaur/enums/FitsHeaderType.java index 1ac2a69..a3bab73 100644 --- a/src/main/java/terrasaur/enums/FitsHeaderType.java +++ b/src/main/java/terrasaur/enums/FitsHeaderType.java @@ -25,31 +25,38 @@ package terrasaur.enums; /** * Enums for a given fits header format. This is used to keep fits headers for the different types * separately configurable. - * + * * @author espirrc1 * */ public enum FitsHeaderType { + ANCIGLOBALGENERIC, + ANCILOCALGENERIC, + ANCIGLOBALALTWG, + ANCIG_FACETRELATION_ALTWG, + ANCILOCALALTWG, + DTMGLOBALALTWG, + DTMLOCALALTWG, + DTMGLOBALGENERIC, + DTMLOCALGENERIC, + NFTMLN; - ANCIGLOBALGENERIC, ANCILOCALGENERIC, ANCIGLOBALALTWG, ANCIG_FACETRELATION_ALTWG, ANCILOCALALTWG, DTMGLOBALALTWG, DTMLOCALALTWG, DTMGLOBALGENERIC, DTMLOCALGENERIC, NFTMLN; + public static boolean isGLobal(FitsHeaderType hdrType) { - public static boolean isGLobal(FitsHeaderType hdrType) { + boolean globalProduct; - boolean globalProduct; + switch (hdrType) { + case ANCIGLOBALALTWG: + case ANCIGLOBALGENERIC: + case DTMGLOBALALTWG: + case DTMGLOBALGENERIC: + globalProduct = true; + break; - switch (hdrType) { + default: + globalProduct = false; + } - case ANCIGLOBALALTWG: - case ANCIGLOBALGENERIC: - case DTMGLOBALALTWG: - case DTMGLOBALGENERIC: - globalProduct = true; - break; - - default: - globalProduct = false; + return globalProduct; } - - return globalProduct; - } } diff --git a/src/main/java/terrasaur/enums/PlaneInfo.java b/src/main/java/terrasaur/enums/PlaneInfo.java index f4dbd7c..8691b8b 100644 --- a/src/main/java/terrasaur/enums/PlaneInfo.java +++ b/src/main/java/terrasaur/enums/PlaneInfo.java @@ -32,147 +32,143 @@ import nom.tam.fits.HeaderCardException; * Enumeration containing the values and comments to use for FITS tags describing data stored in * FITS data cubes. The enumeration name references the type of data stored in a given plane. This * way the user can choose their own value for the FITS keyword (i.e. "PLANE1" or "PLANE10"). - * + * * @author espirrc1 - * + * */ public enum PlaneInfo { - //@formatter:off - LAT("Latitude of vertices", "[deg]", "deg"), - LON("Longitude of vertices", "[deg]", "deg"), - RAD("Radius of vertices", "[km]", "km"), - X("X coordinate of vertices", "[km]", "km"), - Y("Y coordinate of vertices", "[km]", "km"), - Z("Z coordinate of vertices", "[km]", "km"), - NORM_VECTOR_X("Normal vector X", null, null), - NORM_VECTOR_Y("Normal vector Y", null, null), - NORM_VECTOR_Z("Normal vector Z", null, null), - GRAV_VECTOR_X("Gravity vector X", "[m/s^2]", "m/s**2"), - GRAV_VECTOR_Y("Gravity vector Y", "[m/s^2]", "m/s**2"), - GRAV_VECTOR_Z("Gravity vector Z", "[m/s^2]", "m/s**2"), - GRAV_MAG("Gravitational magnitude", "[m/s^2]", "m/s**2"), - GRAV_POT("Gravitational potential", "[J/kg]", "J/kg"), - ELEV("Elevation", "[m]", "m"), - AREA("Area", "[km^2]", "km**2"), + // @formatter:off + LAT("Latitude of vertices", "[deg]", "deg"), + LON("Longitude of vertices", "[deg]", "deg"), + RAD("Radius of vertices", "[km]", "km"), + X("X coordinate of vertices", "[km]", "km"), + Y("Y coordinate of vertices", "[km]", "km"), + Z("Z coordinate of vertices", "[km]", "km"), + NORM_VECTOR_X("Normal vector X", null, null), + NORM_VECTOR_Y("Normal vector Y", null, null), + NORM_VECTOR_Z("Normal vector Z", null, null), + GRAV_VECTOR_X("Gravity vector X", "[m/s^2]", "m/s**2"), + GRAV_VECTOR_Y("Gravity vector Y", "[m/s^2]", "m/s**2"), + GRAV_VECTOR_Z("Gravity vector Z", "[m/s^2]", "m/s**2"), + GRAV_MAG("Gravitational magnitude", "[m/s^2]", "m/s**2"), + GRAV_POT("Gravitational potential", "[J/kg]", "J/kg"), + ELEV("Elevation", "[m]", "m"), + AREA("Area", "[km^2]", "km**2"), - // no longer needed! same as HEIGHT! - // ELEV_NORM("Elevation relative to normal plane", "[m]", "m"), - SLOPE("Slope", "[deg]", "deg"), - SHADE("Shaded relief", null, null), - TILT("Facet tilt", "[deg]", "deg"), - TILT_DIRECTION("Facet tilt direction", "[deg]", "deg"), - TILT_AVERAGE("Mean tilt", "[deg]", "deg"), - TILT_VARIATION("Tilt variation", "[deg]", "deg"), - TILT_AVERAGE_DIRECTION("Mean tilt direction", "[deg]", "deg"), - TILT_DIRECTION_VARIATION("Tilt direction variation", "[deg]", "deg"), - TILT_RELATIVE("Relative tilt", "[deg]", "deg"), - TILT_RELATIVE_DIRECTION("Relative tilt direction", "[deg]", "deg"), - TILT_UNCERTAINTY("Tilt Uncertainty", "[deg]", "deg"), - FACETRAD("Facet radius", "[m]", "m"), - HEIGHT("Height relative to normal plane", "[km]", "km"), - RELATIVE_HEIGHT("Max relative height", "[km]", "km"), - ALBEDO("Relative albedo", null, null), - INTENSITY("Return Intensity", null, null), - SIGMA("Sigma", null, null), - QUALITY("Quality", null, null), - SHADED("Shaded relief", null, null), - NUMPOINTS("Number of OLA points used", null, null), - HEIGHT_RESIDUAL("Mean of residual between points and fitted height", "[km]", "km"), - HEIGHT_STDDEV("Std deviation of residual between points and fitted height", "[km]", "km"), - HAZARD("Hazard", "1 indicates a hazard to the spacecraft", null); - //@formatter:on + // no longer needed! same as HEIGHT! + // ELEV_NORM("Elevation relative to normal plane", "[m]", "m"), + SLOPE("Slope", "[deg]", "deg"), + SHADE("Shaded relief", null, null), + TILT("Facet tilt", "[deg]", "deg"), + TILT_DIRECTION("Facet tilt direction", "[deg]", "deg"), + TILT_AVERAGE("Mean tilt", "[deg]", "deg"), + TILT_VARIATION("Tilt variation", "[deg]", "deg"), + TILT_AVERAGE_DIRECTION("Mean tilt direction", "[deg]", "deg"), + TILT_DIRECTION_VARIATION("Tilt direction variation", "[deg]", "deg"), + TILT_RELATIVE("Relative tilt", "[deg]", "deg"), + TILT_RELATIVE_DIRECTION("Relative tilt direction", "[deg]", "deg"), + TILT_UNCERTAINTY("Tilt Uncertainty", "[deg]", "deg"), + FACETRAD("Facet radius", "[m]", "m"), + HEIGHT("Height relative to normal plane", "[km]", "km"), + RELATIVE_HEIGHT("Max relative height", "[km]", "km"), + ALBEDO("Relative albedo", null, null), + INTENSITY("Return Intensity", null, null), + SIGMA("Sigma", null, null), + QUALITY("Quality", null, null), + SHADED("Shaded relief", null, null), + NUMPOINTS("Number of OLA points used", null, null), + HEIGHT_RESIDUAL("Mean of residual between points and fitted height", "[km]", "km"), + HEIGHT_STDDEV("Std deviation of residual between points and fitted height", "[km]", "km"), + HAZARD("Hazard", "1 indicates a hazard to the spacecraft", null); + // @formatter:on - private String keyValue; // value associated with FITS keyword - private String comment; // comment associated with FITS keyword - private String units; // units associated with the plane. Usually in PDS4 nomenclature + private String keyValue; // value associated with FITS keyword + private String comment; // comment associated with FITS keyword + private String units; // units associated with the plane. Usually in PDS4 nomenclature - PlaneInfo(String keyVal, String comment, String units) { - this.keyValue = keyVal; - this.comment = comment; - this.units = units; - } - - public String value() { - return keyValue; - } - - public String comment() { - return comment; - } - - public String units() { - return units; - } - - /** - * Try to parse the enum from the given Keyval string. Needs to match exactly (but case - * insensitive)! - * - * @param keyVal - * @return - */ - public static PlaneInfo keyVal2Plane(String keyVal) { - for (PlaneInfo planeName : values()) { - if ((planeName.value() != null) && (planeName.value().equalsIgnoreCase(keyVal))) { - return planeName; - } + PlaneInfo(String keyVal, String comment, String units) { + this.keyValue = keyVal; + this.comment = comment; + this.units = units; } - return null; - } - public static PlaneInfo planeFromString(String plane) { - for (PlaneInfo planeName : values()) { - if (planeName.toString().equals(plane)) { - return planeName; - } + public String value() { + return keyValue; } - return null; - } - /* - * Create enumeration set for the first 6 planes. These are the initial planes created from the - * Osiris Rex netCDF file. - */ - public static final EnumSet first6HTags = EnumSet.range(PlaneInfo.LAT, PlaneInfo.Z); - - public static List coreTiltPlanes() { - - List coreTilts = new ArrayList(); - coreTilts.add(PlaneInfo.TILT_AVERAGE); - coreTilts.add(PlaneInfo.TILT_VARIATION); - coreTilts.add(PlaneInfo.TILT_AVERAGE_DIRECTION); - coreTilts.add(PlaneInfo.TILT_DIRECTION_VARIATION); - coreTilts.add(PlaneInfo.TILT_RELATIVE); - coreTilts.add(PlaneInfo.TILT_RELATIVE_DIRECTION); - coreTilts.add(PlaneInfo.RELATIVE_HEIGHT); - - return coreTilts; - - } - - /** - * Convert List to List where each HeaderCard in List follows the - * convention: for each "thisPlane" in List HeaderCard = new HeaderCard("PLANE" + cc, - * thisPlane.value(), thisPlane.comment()) The order in List follows the order in - * List - * - * @param planeList - * @return - * @throws HeaderCardException - */ - public static List planesToHeaderCard(List planeList) - throws HeaderCardException { - List planeHeaders = new ArrayList(); - String plane = "PLANE"; - int cc = 1; - for (PlaneInfo thisPlane : planeList) { - - planeHeaders.add(new HeaderCard(plane + cc, thisPlane.value(), thisPlane.comment())); - cc++; + public String comment() { + return comment; } - return planeHeaders; - } + public String units() { + return units; + } + /** + * Try to parse the enum from the given Keyval string. Needs to match exactly (but case + * insensitive)! + * + * @param keyVal + * @return + */ + public static PlaneInfo keyVal2Plane(String keyVal) { + for (PlaneInfo planeName : values()) { + if ((planeName.value() != null) && (planeName.value().equalsIgnoreCase(keyVal))) { + return planeName; + } + } + return null; + } + + public static PlaneInfo planeFromString(String plane) { + for (PlaneInfo planeName : values()) { + if (planeName.toString().equals(plane)) { + return planeName; + } + } + return null; + } + + /* + * Create enumeration set for the first 6 planes. These are the initial planes created from the + * Osiris Rex netCDF file. + */ + public static final EnumSet first6HTags = EnumSet.range(PlaneInfo.LAT, PlaneInfo.Z); + + public static List coreTiltPlanes() { + + List coreTilts = new ArrayList(); + coreTilts.add(PlaneInfo.TILT_AVERAGE); + coreTilts.add(PlaneInfo.TILT_VARIATION); + coreTilts.add(PlaneInfo.TILT_AVERAGE_DIRECTION); + coreTilts.add(PlaneInfo.TILT_DIRECTION_VARIATION); + coreTilts.add(PlaneInfo.TILT_RELATIVE); + coreTilts.add(PlaneInfo.TILT_RELATIVE_DIRECTION); + coreTilts.add(PlaneInfo.RELATIVE_HEIGHT); + + return coreTilts; + } + + /** + * Convert List to List where each HeaderCard in List follows the + * convention: for each "thisPlane" in List HeaderCard = new HeaderCard("PLANE" + cc, + * thisPlane.value(), thisPlane.comment()) The order in List follows the order in + * List + * + * @param planeList + * @return + * @throws HeaderCardException + */ + public static List planesToHeaderCard(List planeList) throws HeaderCardException { + List planeHeaders = new ArrayList(); + String plane = "PLANE"; + int cc = 1; + for (PlaneInfo thisPlane : planeList) { + + planeHeaders.add(new HeaderCard(plane + cc, thisPlane.value(), thisPlane.comment())); + cc++; + } + return planeHeaders; + } } diff --git a/src/main/java/terrasaur/enums/SigmaFileType.java b/src/main/java/terrasaur/enums/SigmaFileType.java index e4b67a4..6e92136 100644 --- a/src/main/java/terrasaur/enums/SigmaFileType.java +++ b/src/main/java/terrasaur/enums/SigmaFileType.java @@ -27,112 +27,109 @@ import com.google.common.base.Strings; /** * Enum for defining the types of sigma files that can be loaded and utilized by the Pipeline. This * allows the pipeline to load and parse different formats of sigma files. - * + * * @author espirrc1 * */ public enum SigmaFileType { + SPCSIGMA { - SPCSIGMA { - - @Override - public String commentSymbol() { - return ""; - } - - @Override - public String stringArg() { - return "spc"; - } - - @Override - public int sigmaCol() { - return 3; - } - }, - - ERRORFROMSQLSIGMA { - - @Override - public String commentSymbol() { - return "#"; - } - - @Override - public String stringArg() { - return "errorfromsql"; - } - - // should be the Standard Deviation column in ErrorFromSQL file. - public int sigmaCol() { - return 8; - } - }, - - NOMATCH { - - @Override - public String commentSymbol() { - return "NAN"; - } - - @Override - public String stringArg() { - return "NAN"; - } - - public int sigmaCol() { - return -1; - } - }; - - // returns the symbol that is used to denote comment lines - public abstract String commentSymbol(); - - // input argument to match - public abstract String stringArg(); - - // column number where sigma values are stored - public abstract int sigmaCol(); - - public static SigmaFileType getFileType(String sigmaFileType) { - - if (!Strings.isNullOrEmpty(sigmaFileType)) { - for (SigmaFileType thisType : values()) { - - if (sigmaFileType.toLowerCase().equals(thisType.stringArg())) { - - return thisType; + @Override + public String commentSymbol() { + return ""; } - } - } - return NOMATCH; - } - /** - * Return the SigmaFileType associated with the SrcProductType. - * - * @param srcType - * @return - */ - public static SigmaFileType sigmaTypeFromSrcType(SrcProductType srcType) { - SigmaFileType sigmaType = SigmaFileType.NOMATCH; - switch (srcType) { + @Override + public String stringArg() { + return "spc"; + } - case SPC: - sigmaType = SigmaFileType.SPCSIGMA; - break; + @Override + public int sigmaCol() { + return 3; + } + }, - case OLA: - sigmaType = SigmaFileType.ERRORFROMSQLSIGMA; - break; + ERRORFROMSQLSIGMA { - default: - sigmaType = SigmaFileType.NOMATCH; - break; + @Override + public String commentSymbol() { + return "#"; + } + @Override + public String stringArg() { + return "errorfromsql"; + } + + // should be the Standard Deviation column in ErrorFromSQL file. + public int sigmaCol() { + return 8; + } + }, + + NOMATCH { + + @Override + public String commentSymbol() { + return "NAN"; + } + + @Override + public String stringArg() { + return "NAN"; + } + + public int sigmaCol() { + return -1; + } + }; + + // returns the symbol that is used to denote comment lines + public abstract String commentSymbol(); + + // input argument to match + public abstract String stringArg(); + + // column number where sigma values are stored + public abstract int sigmaCol(); + + public static SigmaFileType getFileType(String sigmaFileType) { + + if (!Strings.isNullOrEmpty(sigmaFileType)) { + for (SigmaFileType thisType : values()) { + + if (sigmaFileType.toLowerCase().equals(thisType.stringArg())) { + + return thisType; + } + } + } + return NOMATCH; } - return sigmaType; - } + /** + * Return the SigmaFileType associated with the SrcProductType. + * + * @param srcType + * @return + */ + public static SigmaFileType sigmaTypeFromSrcType(SrcProductType srcType) { + SigmaFileType sigmaType = SigmaFileType.NOMATCH; + switch (srcType) { + case SPC: + sigmaType = SigmaFileType.SPCSIGMA; + break; + + case OLA: + sigmaType = SigmaFileType.ERRORFROMSQLSIGMA; + break; + + default: + sigmaType = SigmaFileType.NOMATCH; + break; + } + + return sigmaType; + } } diff --git a/src/main/java/terrasaur/enums/SrcProductType.java b/src/main/java/terrasaur/enums/SrcProductType.java index 5cd8f87..06243f2 100644 --- a/src/main/java/terrasaur/enums/SrcProductType.java +++ b/src/main/java/terrasaur/enums/SrcProductType.java @@ -25,91 +25,86 @@ package terrasaur.enums; /** * Enumeration storing the source product type: the product type of the source data used in creation * of an ALTWG product. - * + * * @author espirrc1 * */ public enum SrcProductType { + SFM { - SFM { + @Override + public String getAltwgFrag() { + return "sfm"; + } + }, - @Override - public String getAltwgFrag() { - return "sfm"; + SPC { + @Override + public String getAltwgFrag() { + return "spc"; + } + }, + + // OLA Altimetry + OLA { + @Override + public String getAltwgFrag() { + return "alt"; + } + }, + + // SPC-OLA + SPO { + @Override + public String getAltwgFrag() { + return "spo"; + } + }, + + TRUTH { + @Override + public String getAltwgFrag() { + return "tru"; + } + }, + + UNKNOWN { + @Override + public String getAltwgFrag() { + return "unk"; + } + }; + + public static SrcProductType getType(String value) { + value = value.toUpperCase(); + for (SrcProductType srcType : values()) { + if (srcType.toString().equals(value)) { + return srcType; + } + } + return UNKNOWN; } - }, + /** + * Returns the string fragment associated with the source product type. Follows the ALTWG naming + * convention. + * + * @return + */ + public abstract String getAltwgFrag(); - SPC { - @Override - public String getAltwgFrag() { - return "spc"; + /** + * Return the SrcProductType whose getAltwgFrag() string matches the stringFrag. Return UNKNOWN if + * a match is not found. + * + * @param stringFrag + * @return + */ + public static SrcProductType fromAltwgFrag(String stringFrag) { + + for (SrcProductType prodType : SrcProductType.values()) { + if (prodType.getAltwgFrag().equals(stringFrag)) return prodType; + } + return UNKNOWN; } - }, - - // OLA Altimetry - OLA { - @Override - public String getAltwgFrag() { - return "alt"; - } - }, - - // SPC-OLA - SPO { - @Override - public String getAltwgFrag() { - return "spo"; - } - }, - - TRUTH { - @Override - public String getAltwgFrag() { - return "tru"; - } - - }, - - UNKNOWN { - @Override - public String getAltwgFrag() { - return "unk"; - } - }; - - public static SrcProductType getType(String value) { - value = value.toUpperCase(); - for (SrcProductType srcType : values()) { - if (srcType.toString().equals(value)) { - return srcType; - } - } - return UNKNOWN; - } - - /** - * Returns the string fragment associated with the source product type. Follows the ALTWG naming - * convention. - * - * @return - */ - public abstract String getAltwgFrag(); - - /** - * Return the SrcProductType whose getAltwgFrag() string matches the stringFrag. Return UNKNOWN if - * a match is not found. - * - * @param stringFrag - * @return - */ - public static SrcProductType fromAltwgFrag(String stringFrag) { - - for (SrcProductType prodType : SrcProductType.values()) { - if (prodType.getAltwgFrag().equals(stringFrag)) - return prodType; - } - return UNKNOWN; - } - } diff --git a/src/main/java/terrasaur/fits/AltPipelnEnum.java b/src/main/java/terrasaur/fits/AltPipelnEnum.java index d14a062..2f7fdcf 100644 --- a/src/main/java/terrasaur/fits/AltPipelnEnum.java +++ b/src/main/java/terrasaur/fits/AltPipelnEnum.java @@ -27,298 +27,344 @@ import java.util.Map; public enum AltPipelnEnum { - // settings enums. Used to determine type of product to create. - ANCIGLOBAL, + // settings enums. Used to determine type of product to create. + ANCIGLOBAL, - // optional: tells code to skip generation of products for highest res shape model - SKIPORIGINALSHP, + // optional: tells code to skip generation of products for highest res shape model + SKIPORIGINALSHP, - // controls whether or not certain global products get created. - // REQUIRED TO BE IN CONFIGFILE: - DOGLOBALSHAPE, OBJTOFITS, ADDOBJCOMMENTS, GLOBALRES0, DUMBERVALUES, DOGRAVGLOBAL, DOGLOBALGRAV_ANCI, DOGLOBALTILT_ANCI, DOGLOBALMISC_ANCI, DOGLOBALTILT, + // controls whether or not certain global products get created. + // REQUIRED TO BE IN CONFIGFILE: + DOGLOBALSHAPE, + OBJTOFITS, + ADDOBJCOMMENTS, + GLOBALRES0, + DUMBERVALUES, + DOGRAVGLOBAL, + DOGLOBALGRAV_ANCI, + DOGLOBALTILT_ANCI, + DOGLOBALMISC_ANCI, + DOGLOBALTILT, - // controls number of slots per job to use when running global distributed gravity - // in grid engine mode. Does not apply if running in local parallel mode - GLOBALGRAVSLOTS, + // controls number of slots per job to use when running global distributed gravity + // in grid engine mode. Does not apply if running in local parallel mode + GLOBALGRAVSLOTS, - // controls number of slots per job to use when running local distributed gravity - // in grid engine mode. Does not apply if running in local parallel mode - LOCALGRAVSLOTS, + // controls number of slots per job to use when running local distributed gravity + // in grid engine mode. Does not apply if running in local parallel mode + LOCALGRAVSLOTS, - // full path to SPICE metakernel to use when generating DSK products - DSKERNEL, + // full path to SPICE metakernel to use when generating DSK products + DSKERNEL, - // enable/disable the creation of global and local DSK files - DOGLOBALDSK, DOLOCALDSK, + // enable/disable the creation of global and local DSK files + DOGLOBALDSK, + DOLOCALDSK, - // global tilt semi-major axis in km. Now a required variable - GTILTMAJAXIS, + // global tilt semi-major axis in km. Now a required variable + GTILTMAJAXIS, - // settings for every local product generated by the pipeline - USEOLDMAPLETS, DODTMFITSOBJ, DOGRAVLOCAL, GENLOCALGRAV, DOLOCALTILT, DOLOCAL_GRAVANCI, DOLOCAL_TILTANCI, DOLOCAL_MISCANCI, + // settings for every local product generated by the pipeline + USEOLDMAPLETS, + DODTMFITSOBJ, + DOGRAVLOCAL, + GENLOCALGRAV, + DOLOCALTILT, + DOLOCAL_GRAVANCI, + DOLOCAL_TILTANCI, + DOLOCAL_MISCANCI, - // controls whether to use average gravitational potential for global and local gravity - // calculations. =0 use minimum reference potential, =1 use average reference potential - AVGGRAVPOTGLOBAL, AVGGRAVPOTLOCAL, + // controls whether to use average gravitational potential for global and local gravity + // calculations. =0 use minimum reference potential, =1 use average reference potential + AVGGRAVPOTGLOBAL, + AVGGRAVPOTLOCAL, - // controls RunBigMap. - // integrate slope to height required. Defaults to "n" if this enum does not exist in config file. - // otherwise will evaluate value. 0="n", 1="y" - INTSLOPE, + // controls RunBigMap. + // integrate slope to height required. Defaults to "n" if this enum does not exist in config file. + // otherwise will evaluate value. 0="n", 1="y" + INTSLOPE, - // use grotesque model in RunBigMap. Defaults to not using it if this enum does not exist in - // config file - // otherwise will evaluate value. 0="do not use grotesque model", 1="do use grotesque model" - USEGROTESQUE, + // use grotesque model in RunBigMap. Defaults to not using it if this enum does not exist in + // config file + // otherwise will evaluate value. 0="do not use grotesque model", 1="do use grotesque model" + USEGROTESQUE, - // controls the source data, product destination, naming convention, - // and process flow. - DATASOURCE, REPORTTYPE, INDATADIR, OUTPUTDIR, PDSNAMING, RENAMEGLOBALOBJ, USEBIGMAPS, DOREPORT, + // controls the source data, product destination, naming convention, + // and process flow. + DATASOURCE, + REPORTTYPE, + INDATADIR, + OUTPUTDIR, + PDSNAMING, + RENAMEGLOBALOBJ, + USEBIGMAPS, + DOREPORT, - // shape model density and rotation rate are now required variables. This way we can easily spot - // what we are using - // as defaults. - SMDENSITY, SMRRATE, + // shape model density and rotation rate are now required variables. This way we can easily spot + // what we are using + // as defaults. + SMDENSITY, + SMRRATE, - // stores type of naming convention. Ex. AltProduct, AltNFTMLN, DartProduct. - NAMINGCONVENTION, + // stores type of naming convention. Ex. AltProduct, AltNFTMLN, DartProduct. + NAMINGCONVENTION, - // set values that cannot be derived from data. - REPORTBASENAME, VERSION, + // set values that cannot be derived from data. + REPORTBASENAME, + VERSION, - // everything after this is not a required keyword + // everything after this is not a required keyword - // (Optional) controls whether there is an external body that needs to be accounted for when - // running gravity code - // the values should be a csv string with no spaces. THe values are: mass(kg),x,y,z where x,y,z - // are the body - // fixed coordinates in km. - // e.g. 521951167,1.19,0,0 - EXTERNALBODY, + // (Optional) controls whether there is an external body that needs to be accounted for when + // running gravity code + // the values should be a csv string with no spaces. THe values are: mass(kg),x,y,z where x,y,z + // are the body + // fixed coordinates in km. + // e.g. 521951167,1.19,0,0 + EXTERNALBODY, - // (Optional). If the keyword exists and value is 1 then no GLOBAL DTMs are assumed to be created. - // for example, in the DART Derived Product set we are not creating g_*dtm*.fits files - NOGLOBALDTM, - - // (Optional). If the keyword exists then evaluate the shapes to process by parsing the - // comma-separated values. Ex.values are 0,1,2 then pipeline will assume it has to - // process shape0, shape1, shape2. The pipeline will also disregard the values in - // DUMBERVALUES that otherwise determine how many shape files to process. - SHAPE2PROC, - - // (optional) controls whether or not STL files get generated. If these do not exist in the - // pipeConfig file then they will NOT - // get generated! - GLOBALSTL, LOCALSTL, + // (Optional). If the keyword exists and value is 1 then no GLOBAL DTMs are assumed to be created. + // for example, in the DART Derived Product set we are not creating g_*dtm*.fits files + NOGLOBALDTM, - // keywords for local products - // - // MAPSmallerSZ: resize local DTMs to a different half size. For pipeline we may want to generate - // DTMs at halfsize + tilt radius then resize the DTMs to halfsize in order to have tilts - // evaluated with the full range of points at the edges. - // - // MAPFILE: contains pointer to map centers file (optional). Used by TileShapeModelWithBigmaps. - // defaults to auto-generated tiles if this is not specified. - // allow for pointers to different files for 30cm and 10cm map products. - DOLOCALMAP, MAPDIR, MAPSCALE, MAPHALFSZ, REPORT, MAPSmallerSZ, MAPFILE, ISTAGSITE, LTILTMAJAXIS, LTILTMINAXIS, LTILTPA, MAXSCALE, + // (Optional). If the keyword exists then evaluate the shapes to process by parsing the + // comma-separated values. Ex.values are 0,1,2 then pipeline will assume it has to + // process shape0, shape1, shape2. The pipeline will also disregard the values in + // DUMBERVALUES that otherwise determine how many shape files to process. + SHAPE2PROC, - // settings for local tag sites. Note TAGSFILE is not optional. - // it contains the tagsite name and lat,lon of tag site tile center - TAGDIR, TAGSCALE, TAGHALFSZ, TAGSFILE, REPORTTAG, + // (optional) controls whether or not STL files get generated. If these do not exist in the + // pipeConfig file then they will NOT + // get generated! + GLOBALSTL, + LOCALSTL, - // pointer to OLA database. only required if DATASOURCE is OLA - OLADBPATH, + // keywords for local products + // + // MAPSmallerSZ: resize local DTMs to a different half size. For pipeline we may want to generate + // DTMs at halfsize + tilt radius then resize the DTMs to halfsize in order to have tilts + // evaluated with the full range of points at the edges. + // + // MAPFILE: contains pointer to map centers file (optional). Used by TileShapeModelWithBigmaps. + // defaults to auto-generated tiles if this is not specified. + // allow for pointers to different files for 30cm and 10cm map products. + DOLOCALMAP, + MAPDIR, + MAPSCALE, + MAPHALFSZ, + REPORT, + MAPSmallerSZ, + MAPFILE, + ISTAGSITE, + LTILTMAJAXIS, + LTILTMINAXIS, + LTILTPA, + MAXSCALE, - // force sigma files to all be NaN - FORCESIGMA_NAN, + // settings for local tag sites. Note TAGSFILE is not optional. + // it contains the tagsite name and lat,lon of tag site tile center + TAGDIR, + TAGSCALE, + TAGHALFSZ, + TAGSFILE, + REPORTTAG, - // global sigma scale factor - SIGMA_SCALEFACTOR, + // pointer to OLA database. only required if DATASOURCE is OLA + OLADBPATH, - // local sigma scale factor - LOCAL_SIGMA_SCALEFACTOR, + // force sigma files to all be NaN + FORCESIGMA_NAN, - // SIGMA file type. No longer tied to DataSource! - SIGMAFILE_TYPE, + // global sigma scale factor + SIGMA_SCALEFACTOR, - // force the Report page to be HTML. Default is created at PHP - REPORTASHTML, + // local sigma scale factor + LOCAL_SIGMA_SCALEFACTOR, - /* - * The following are used to change default values used by the pipeline these are the shape model - * density, rotation rate, gravitational algorithm, gravitational constant, global average - * reference potential, local average reference potential. Added values defining the tilt ellipse - * to use when evaluating tilts. Note the different enums for global versus local tilt ellipse - * parameters. The pipeline will use default values for these enums if they are not defined in the - * pipeline configuration file. - */ - GALGORITHM, GRAVCONST, GTILTMINAXIS, GTILTPA, MASSUNCERT, VSEARCHRAD_PCTGSD, FSEARCHRAD_PCTGSD, PXPERDEG, + // SIGMA file type. No longer tied to DataSource! + SIGMAFILE_TYPE, - // The following are options to subvert normal pipeline operations or to configure pipeline for - // other missions + // force the Report page to be HTML. Default is created at PHP + REPORTASHTML, + /* + * The following are used to change default values used by the pipeline these are the shape model + * density, rotation rate, gravitational algorithm, gravitational constant, global average + * reference potential, local average reference potential. Added values defining the tilt ellipse + * to use when evaluating tilts. Note the different enums for global versus local tilt ellipse + * parameters. The pipeline will use default values for these enums if they are not defined in the + * pipeline configuration file. + */ + GALGORITHM, + GRAVCONST, + GTILTMINAXIS, + GTILTPA, + MASSUNCERT, + VSEARCHRAD_PCTGSD, + FSEARCHRAD_PCTGSD, + PXPERDEG, - // global objs are supplied at all resolutions as the starting point. - // This means we can skip ICQ2PLT, ICQDUMBER, and PLT2OBJ calls - OBJASINPUT, + // The following are options to subvert normal pipeline operations or to configure pipeline for + // other missions - // gzip the obj files to save space - DOGZIP, + // global objs are supplied at all resolutions as the starting point. + // This means we can skip ICQ2PLT, ICQDUMBER, and PLT2OBJ calls + OBJASINPUT, - // specify the queue to use in the GRID ENGINE - GRIDQUEUE, + // gzip the obj files to save space + DOGZIP, - // default mode for local product creation is to parallelize DistributedGravity - // for each tile. Then processing for each job is done in local mode. - // set this flag to 1 to submit DistributedGravity for each tile sequentially, - // and have each job spawn to the grid engine - DISTGRAVITY_USEGRID, + // specify the queue to use in the GRID ENGINE + GRIDQUEUE, - // override grid engine mode and use local parallel mode with the specified number of cores - LOCALPARALLEL, + // default mode for local product creation is to parallelize DistributedGravity + // for each tile. Then processing for each job is done in local mode. + // set this flag to 1 to submit DistributedGravity for each tile sequentially, + // and have each job spawn to the grid engine + DISTGRAVITY_USEGRID, - // when creating local gravity products skip creation of gravity files that already exist - USEOLDGRAV, + // override grid engine mode and use local parallel mode with the specified number of cores + LOCALPARALLEL, - // override ancillary fits table default setting (binary). Set to ASCII instead - ANCITXTTABLE, + // when creating local gravity products skip creation of gravity files that already exist + USEOLDGRAV, - // contains pointer to fits header config file (optional) - FITSCONFIGFILE, + // override ancillary fits table default setting (binary). Set to ASCII instead + ANCITXTTABLE, - // contains pointer to OBJ comments header file (optional). Will only - // add commentes if ADDOBJCOMMENTS flag is set. - OBJCOMHEADER, + // contains pointer to fits header config file (optional) + FITSCONFIGFILE, - // identifies whether there are poisson reconstruct data products to include in the webpage report - LOCALPOISSON, GLOBALPOISSON; + // contains pointer to OBJ comments header file (optional). Will only + // add commentes if ADDOBJCOMMENTS flag is set. + OBJCOMHEADER, - /* - * The following enumerations are required to exist in the altwg pipeline config file. - */ - public static final EnumSet reqTags = EnumSet.range(DOGLOBALSHAPE, VERSION); + // identifies whether there are poisson reconstruct data products to include in the webpage report + LOCALPOISSON, + GLOBALPOISSON; - /* - * The following enumerations do not have to be present in the config file. But, if they are not - * then the pipeline should use the default values associated with the enums. - */ - public static final EnumSet overrideTags = EnumSet.range(SMDENSITY, GTILTPA); + /* + * The following enumerations are required to exist in the altwg pipeline config file. + */ + public static final EnumSet reqTags = EnumSet.range(DOGLOBALSHAPE, VERSION); - public static String mapToString(Map pipeConfig) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : pipeConfig.entrySet()) { - sb.append(String.format("%s:%s\n", entry.getKey().toString(), entry.getValue())); + /* + * The following enumerations do not have to be present in the config file. But, if they are not + * then the pipeline should use the default values associated with the enums. + */ + public static final EnumSet overrideTags = EnumSet.range(SMDENSITY, GTILTPA); + + public static String mapToString(Map pipeConfig) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : pipeConfig.entrySet()) { + sb.append(String.format("%s:%s\n", entry.getKey().toString(), entry.getValue())); + } + + return sb.toString(); } - return sb.toString(); - } - - public static AltPipelnEnum fromString(String textString) { - for (AltPipelnEnum anciType : AltPipelnEnum.values()) { - if (anciType.toString().equals(textString)) - return anciType; + public static AltPipelnEnum fromString(String textString) { + for (AltPipelnEnum anciType : AltPipelnEnum.values()) { + if (anciType.toString().equals(textString)) return anciType; + } + return null; } - return null; - } - /** - * Convenience method for evaluating configuration parameter where the value indicates whether or - * not the parameter is true or false via an integer value. Assumes 0 or less = false, 1 or more = - * true. If the key does not exist then return false. - * - * @param pipeConfig - * @param key - * @return - */ - public static boolean isTrue(Map pipeConfig, AltPipelnEnum key) { - boolean returnFlag = false; - int parsedVal = 0; - if (pipeConfig.containsKey(key)) { - try { - parsedVal = Integer.valueOf(pipeConfig.get(key)); - } catch (NumberFormatException e) { - System.err.println("ERROR! Could not parse integer value for pipeConfig line:"); - System.err.println(key.toString() + "," + pipeConfig.get(key)); - System.err.println("Stopping with error!"); - System.exit(1); - } - - if (parsedVal > 0) { - returnFlag = true; - } + /** + * Convenience method for evaluating configuration parameter where the value indicates whether or + * not the parameter is true or false via an integer value. Assumes 0 or less = false, 1 or more = + * true. If the key does not exist then return false. + * + * @param pipeConfig + * @param key + * @return + */ + public static boolean isTrue(Map pipeConfig, AltPipelnEnum key) { + boolean returnFlag = false; + int parsedVal = 0; + if (pipeConfig.containsKey(key)) { + try { + parsedVal = Integer.valueOf(pipeConfig.get(key)); + } catch (NumberFormatException e) { + System.err.println("ERROR! Could not parse integer value for pipeConfig line:"); + System.err.println(key.toString() + "," + pipeConfig.get(key)); + System.err.println("Stopping with error!"); + System.exit(1); + } + if (parsedVal > 0) { + returnFlag = true; + } + } + return returnFlag; } - return returnFlag; - } - /** - * Checks to see whether key exists. If so then return value mapped to key. Otherwise return empty - * string. - * - * @param pipeConfig - * @param key - * @return - */ - public static String checkAndGet(Map pipeConfig, AltPipelnEnum key) { - String value = ""; - if (pipeConfig.containsKey(key)) { - value = pipeConfig.get(key); + /** + * Checks to see whether key exists. If so then return value mapped to key. Otherwise return empty + * string. + * + * @param pipeConfig + * @param key + * @return + */ + public static String checkAndGet(Map pipeConfig, AltPipelnEnum key) { + String value = ""; + if (pipeConfig.containsKey(key)) { + value = pipeConfig.get(key); + } + return value; } - return value; - } - /* - * Some enums will have a default value, e.g. the ones in the overrideTags EnumSet. It is easier - * to keep them as string values then convert them to other primitives as needed. Sometimes other - * executables will be called w/ the default values, so it is better to keep them as strings to - * avoid double conversion. - */ - public static String getDefault(AltPipelnEnum thisEnum) { + /* + * Some enums will have a default value, e.g. the ones in the overrideTags EnumSet. It is easier + * to keep them as string values then convert them to other primitives as needed. Sometimes other + * executables will be called w/ the default values, so it is better to keep them as strings to + * avoid double conversion. + */ + public static String getDefault(AltPipelnEnum thisEnum) { - if (overrideTags.contains(thisEnum)) { + if (overrideTags.contains(thisEnum)) { - switch (thisEnum) { + switch (thisEnum) { - // shape model density and rotation rate must now be explicitly defined in the configuration - // file! - // case SMDENSITY: - // return "1.186"; - // - // case SMRRATE: - // return "0.00040626"; + // shape model density and rotation rate must now be explicitly defined in the configuration + // file! + // case SMDENSITY: + // return "1.186"; + // + // case SMRRATE: + // return "0.00040626"; - case GALGORITHM: - return "werner"; + case GALGORITHM: + return "werner"; - case GRAVCONST: - return "6.67408e-11"; + case GRAVCONST: + return "6.67408e-11"; - case LTILTMAJAXIS: - return "0.0125"; + case LTILTMAJAXIS: + return "0.0125"; - case GTILTMINAXIS: - return "0.0125"; + case GTILTMINAXIS: + return "0.0125"; - case GTILTPA: - case LTILTPA: - return "0.0"; + case GTILTPA: + case LTILTPA: + return "0.0"; - case MASSUNCERT: - return "0.01"; + case MASSUNCERT: + return "0.01"; - case VSEARCHRAD_PCTGSD: - return "0.25"; + case VSEARCHRAD_PCTGSD: + return "0.25"; - case FSEARCHRAD_PCTGSD: - return "0.5"; + case FSEARCHRAD_PCTGSD: + return "0.5"; - default: - return "NA"; - - } + default: + return "NA"; + } + } + return "NA"; } - return "NA"; - - }; - + ; } diff --git a/src/main/java/terrasaur/fits/AltwgAnciGlobal.java b/src/main/java/terrasaur/fits/AltwgAnciGlobal.java index d20d32c..d6bfa4a 100644 --- a/src/main/java/terrasaur/fits/AltwgAnciGlobal.java +++ b/src/main/java/terrasaur/fits/AltwgAnciGlobal.java @@ -30,53 +30,49 @@ import terrasaur.enums.FitsHeaderType; public class AltwgAnciGlobal extends AnciTableFits implements AnciFitsHeader { - public AltwgAnciGlobal(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.ANCIGLOBALALTWG); - } - - // methods below override the concrete methods in AnciTableFits abstract class or - // are specific to this class - - /** - * Create fits header as a list of HeaderCard. List contains the keywords in the order of - * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. - */ - @Override - public List createFitsHeader() throws HeaderCardException { - - List headers = new ArrayList(); - - headers.addAll(getHeaderInfo("header information")); - headers.addAll(getMissionInfo("mission information")); - headers.addAll(getIDInfo("identification info")); - headers.addAll(getMapDataSrc("shape data source")); - headers.addAll(getProcInfo("processing information")); - headers.addAll(getMapInfo("map specific information")); - headers.addAll(getSpatialInfo("summary spatial information")); - headers.addAll(getSpecificInfo("product specific")); - - return headers; - - } - - /** - * Contains OREX-SPOC specific keywords. - */ - @Override - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public AltwgAnciGlobal(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.ANCIGLOBALALTWG); } - headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - return headers; + // methods below override the concrete methods in AnciTableFits abstract class or + // are specific to this class - } + /** + * Create fits header as a list of HeaderCard. List contains the keywords in the order of + * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. + */ + @Override + public List createFitsHeader() throws HeaderCardException { + List headers = new ArrayList(); + headers.addAll(getHeaderInfo("header information")); + headers.addAll(getMissionInfo("mission information")); + headers.addAll(getIDInfo("identification info")); + headers.addAll(getMapDataSrc("shape data source")); + headers.addAll(getProcInfo("processing information")); + headers.addAll(getMapInfo("map specific information")); + headers.addAll(getSpatialInfo("summary spatial information")); + headers.addAll(getSpecificInfo("product specific")); + + return headers; + } + + /** + * Contains OREX-SPOC specific keywords. + */ + @Override + public List getIDInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/AltwgAnciGlobalFacetRelation.java b/src/main/java/terrasaur/fits/AltwgAnciGlobalFacetRelation.java index 8e696ea..47b51bb 100644 --- a/src/main/java/terrasaur/fits/AltwgAnciGlobalFacetRelation.java +++ b/src/main/java/terrasaur/fits/AltwgAnciGlobalFacetRelation.java @@ -30,67 +30,66 @@ import terrasaur.enums.FitsHeaderType; public class AltwgAnciGlobalFacetRelation extends AnciTableFits implements AnciFitsHeader { - public AltwgAnciGlobalFacetRelation(FitsHdr fitsHeader) { + public AltwgAnciGlobalFacetRelation(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.ANCIG_FACETRELATION_ALTWG); - } - - // methods below override the concrete methods in AnciTableFits abstract class or - // are specific to this class - - /** - * Create fits header as a list of HeaderCard. List contains the keywords in the order of - * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. - */ - @Override - public List createFitsHeader() throws HeaderCardException { - - List headers = new ArrayList(); - - headers.addAll(getHeaderInfo("header information")); - headers.addAll(getMissionInfo("mission information")); - headers.addAll(getIDInfo("identification info")); - headers.addAll(getMapDataSrc("shape data source")); - headers.addAll(getProcInfo("processing information")); - headers.addAll(getMapInfo("map specific information")); - headers.addAll(getSpatialInfo("summary spatial information")); - headers.addAll(getSpecificInfo("product specific")); - - return headers; - - } - - /** - * Return the HeaderCards associated with a specific product. By default we use the ALTWG specific - * product keywords - * - * @param comment - * @return - * @throws HeaderCardException - */ - @Override - public List getSpecificInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + super(fitsHeader, FitsHeaderType.ANCIG_FACETRELATION_ALTWG); } - headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJINDX)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDINDX)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDINDXI)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIGMA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DSIG_DEF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DENSITY)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ROT_RATE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.REF_POT)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MAJ)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MIN)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_PA)); + // methods below override the concrete methods in AnciTableFits abstract class or + // are specific to this class - return headers; - } + /** + * Create fits header as a list of HeaderCard. List contains the keywords in the order of + * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. + */ + @Override + public List createFitsHeader() throws HeaderCardException { + + List headers = new ArrayList(); + + headers.addAll(getHeaderInfo("header information")); + headers.addAll(getMissionInfo("mission information")); + headers.addAll(getIDInfo("identification info")); + headers.addAll(getMapDataSrc("shape data source")); + headers.addAll(getProcInfo("processing information")); + headers.addAll(getMapInfo("map specific information")); + headers.addAll(getSpatialInfo("summary spatial information")); + headers.addAll(getSpecificInfo("product specific")); + + return headers; + } + + /** + * Return the HeaderCards associated with a specific product. By default we use the ALTWG specific + * product keywords + * + * @param comment + * @return + * @throws HeaderCardException + */ + @Override + public List getSpecificInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + + headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJINDX)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDINDX)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDINDXI)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIGMA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DSIG_DEF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DENSITY)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ROT_RATE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.REF_POT)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MAJ)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MIN)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_PA)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/AltwgAnciLocal.java b/src/main/java/terrasaur/fits/AltwgAnciLocal.java index 41469bb..ad82cc3 100644 --- a/src/main/java/terrasaur/fits/AltwgAnciLocal.java +++ b/src/main/java/terrasaur/fits/AltwgAnciLocal.java @@ -30,53 +30,49 @@ import terrasaur.enums.FitsHeaderType; public class AltwgAnciLocal extends AnciTableFits implements AnciFitsHeader { - public AltwgAnciLocal(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.ANCILOCALALTWG); - } - - // methods below override the concrete methods in AnciTableFits abstract class or - // are specific to this class - - /** - * Create fits header as a list of HeaderCard. List contains the keywords in the order of - * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. - */ - @Override - public List createFitsHeader() throws HeaderCardException { - - List headers = new ArrayList(); - - headers.addAll(getHeaderInfo("header information")); - headers.addAll(getMissionInfo("mission information")); - headers.addAll(getIDInfo("identification info")); - headers.addAll(getMapDataSrc("shape data source")); - headers.addAll(getProcInfo("processing information")); - headers.addAll(getMapInfo("map specific information")); - headers.addAll(getSpatialInfo("summary spatial information")); - headers.addAll(getSpecificInfo("product specific")); - - return headers; - - } - - /** - * Contains OREX-SPOC specific keywords. - */ - @Override - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public AltwgAnciLocal(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.ANCILOCALALTWG); } - headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - return headers; + // methods below override the concrete methods in AnciTableFits abstract class or + // are specific to this class - } + /** + * Create fits header as a list of HeaderCard. List contains the keywords in the order of + * appearance in the ALTWG fits header. Overrides default implementation in AnciTableFits. + */ + @Override + public List createFitsHeader() throws HeaderCardException { + List headers = new ArrayList(); + headers.addAll(getHeaderInfo("header information")); + headers.addAll(getMissionInfo("mission information")); + headers.addAll(getIDInfo("identification info")); + headers.addAll(getMapDataSrc("shape data source")); + headers.addAll(getProcInfo("processing information")); + headers.addAll(getMapInfo("map specific information")); + headers.addAll(getSpatialInfo("summary spatial information")); + headers.addAll(getSpecificInfo("product specific")); + + return headers; + } + + /** + * Contains OREX-SPOC specific keywords. + */ + @Override + public List getIDInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/AltwgGlobalDTM.java b/src/main/java/terrasaur/fits/AltwgGlobalDTM.java index e3ad458..f18cb29 100644 --- a/src/main/java/terrasaur/fits/AltwgGlobalDTM.java +++ b/src/main/java/terrasaur/fits/AltwgGlobalDTM.java @@ -33,77 +33,73 @@ import terrasaur.utils.DTMHeader; * Contains methods for building fits header corresponding to ALTWG Global DTM. Methods that are * specific to the ALTWG Global DTM fits header are contained here. Default methods contained in * DTMFits class. - * + * * @author espirrc1 * */ public class AltwgGlobalDTM extends DTMFits implements DTMHeader { - public AltwgGlobalDTM(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.DTMGLOBALALTWG); - } - - /** - * Fits header block containing observation or ID related information. Includes keywords specific - * to OREX-SPOC - * - * @return - * @throws HeaderCardException - */ - @Override - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public AltwgGlobalDTM(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.DTMGLOBALALTWG); } - headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - return headers; + /** + * Fits header block containing observation or ID related information. Includes keywords specific + * to OREX-SPOC + * + * @return + * @throws HeaderCardException + */ + @Override + public List getIDInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - /** - * return Fits header block that contains information about the fits header itself. Custom to - * OREX-SPOC - * - * @return - * @throws HeaderCardException - */ - @Override - public List getHeaderInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headers; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - return headers; + /** + * return Fits header block that contains information about the fits header itself. Custom to + * OREX-SPOC + * + * @return + * @throws HeaderCardException + */ + @Override + public List getHeaderInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - - /** - * Added GSDI - specific to OREX-SPOC. - */ - @Override - public List getMapInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headers; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); - return headers; - } + /** + * Added GSDI - specific to OREX-SPOC. + */ + @Override + public List getMapInfo(String comment) throws HeaderCardException { + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/AltwgLocalDTM.java b/src/main/java/terrasaur/fits/AltwgLocalDTM.java index 0e056d6..24de0a4 100644 --- a/src/main/java/terrasaur/fits/AltwgLocalDTM.java +++ b/src/main/java/terrasaur/fits/AltwgLocalDTM.java @@ -33,122 +33,119 @@ import terrasaur.utils.DTMHeader; * Contains methods for building fits header corresponding to ALTWG local DTM. Methods that are * specific to the ALTWG Local DTM fits header are contained here. Default methods are contained in * DTMFits class. - * + * * @author espirrc1 * */ public class AltwgLocalDTM extends DTMFits implements DTMHeader { - public AltwgLocalDTM(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.DTMLOCALALTWG); - } - - /** - * Fits header block containing observation or ID related information. Includes keywords specific - * to OREX-SPOC - * - * @return - * @throws HeaderCardException - */ - @Override - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - - return headers; - - } - - @Override - public List getSpecificInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public AltwgLocalDTM(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.DTMLOCALALTWG); } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DQUAL_1)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DQUAL_2)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.PXPERDEG)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DENSITY)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.ROT_RATE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.REF_POT)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MAJ)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MIN)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_PA)); + /** + * Fits header block containing observation or ID related information. Includes keywords specific + * to OREX-SPOC + * + * @return + * @throws HeaderCardException + */ + @Override + public List getIDInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - /** - * Include corner points, center vector and ux,uy,uz describing local plane - */ - @Override - public List getSpatialInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headers; } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + @Override + public List getSpecificInfo(String comment) throws HeaderCardException { - headers.addAll(getCornerCards()); - headers.addAll(getCenterVec()); - headers.addAll(getUX()); - headers.addAll(getUY()); - headers.addAll(getUZ()); + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } - return headers; - } + headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DQUAL_1)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DQUAL_2)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.PXPERDEG)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DENSITY)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.ROT_RATE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.REF_POT)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MAJ)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MIN)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_PA)); - /** - * return Fits header block that contains information about the fits header itself. Custom to - * OREX-SPOC - * - * @return - * @throws HeaderCardException - */ - @Override - public List getHeaderInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headers; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - return headers; + /** + * Include corner points, center vector and ux,uy,uz describing local plane + */ + @Override + public List getSpatialInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } - /** - * Added GSDI - specific to OREX-SPOC. - */ - @Override - public List getMapInfo(String comment) throws HeaderCardException { + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + headers.addAll(getCornerCards()); + headers.addAll(getCenterVec()); + headers.addAll(getUX()); + headers.addAll(getUY()); + headers.addAll(getUZ()); + + return headers; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); - return headers; - } + /** + * return Fits header block that contains information about the fits header itself. Custom to + * OREX-SPOC + * + * @return + * @throws HeaderCardException + */ + @Override + public List getHeaderInfo(String comment) throws HeaderCardException { + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); + + return headers; + } + + /** + * Added GSDI - specific to OREX-SPOC. + */ + @Override + public List getMapInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/AnciFitsHeader.java b/src/main/java/terrasaur/fits/AnciFitsHeader.java index 9c120c8..65d7d17 100644 --- a/src/main/java/terrasaur/fits/AnciFitsHeader.java +++ b/src/main/java/terrasaur/fits/AnciFitsHeader.java @@ -23,12 +23,10 @@ package terrasaur.fits; import java.util.List; - import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; public interface AnciFitsHeader { - public List createFitsHeader() throws HeaderCardException; - + public List createFitsHeader() throws HeaderCardException; } diff --git a/src/main/java/terrasaur/fits/AnciTableFits.java b/src/main/java/terrasaur/fits/AnciTableFits.java index 4795444..90cd8b8 100644 --- a/src/main/java/terrasaur/fits/AnciTableFits.java +++ b/src/main/java/terrasaur/fits/AnciTableFits.java @@ -32,282 +32,272 @@ import terrasaur.enums.FitsHeaderType; * Abstract generic class with concrete methods and attributes for creating a FITS table with * generalized fits header. Specific implementations can be written to create custom fits headers as * needed. - * + * * @author espirrc1 * */ public abstract class AnciTableFits { - public final String COMMENT = "COMMENT"; - FitsHdr fitsHdr; - public final FitsHeaderType fitsHeaderType; + public final String COMMENT = "COMMENT"; + FitsHdr fitsHdr; + public final FitsHeaderType fitsHeaderType; - public AnciTableFits(FitsHdr fitsHdr, FitsHeaderType fitsHeaderType) { - this.fitsHdr = fitsHdr; - this.fitsHeaderType = fitsHeaderType; - } - - public List createFitsHeader() throws HeaderCardException { - - List headers = new ArrayList(); - - headers.addAll(getHeaderInfo("header information")); - headers.addAll(getMissionInfo("mission information")); - headers.addAll(getIDInfo("identification info")); - headers.addAll(getMapDataSrc("shape data source")); - headers.addAll(getProcInfo("processing information")); - headers.addAll(getMapInfo("map specific information")); - headers.addAll(getSpatialInfo("summary spatial information")); - - return headers; - - } - - /** - * return Fits header block that contains information about the fits header itself. - * - * @return - * @throws HeaderCardException - */ - public List getHeaderInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - - return headers; - - } - - /** - * Fits header block containing information about the mission. - * - * @return - * @throws HeaderCardException - */ - public List getMissionInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); - - return headers; - - } - - /** - * Fits header block containing observation or ID related information. - * - * @return - * @throws HeaderCardException - */ - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - - return headers; - - } - - /** - * Fits header block containing information about the source data used to create the map. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getMapDataSrc(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.CREATOR)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); - return headers; - - } - - public List getMapInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); - - return headers; - } - - /** - * Fits header block containing information about the software processing done to generate the - * product. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getProcInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - - return headers; - - } - - public List getSpatialInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public AnciTableFits(FitsHdr fitsHdr, FitsHeaderType fitsHeaderType) { + this.fitsHdr = fitsHdr; + this.fitsHeaderType = fitsHeaderType; } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + public List createFitsHeader() throws HeaderCardException { - headers.addAll(getCornerCards()); + List headers = new ArrayList(); - // add CNTR_V_X,Y,Z - headers.addAll(getCenterVec()); + headers.addAll(getHeaderInfo("header information")); + headers.addAll(getMissionInfo("mission information")); + headers.addAll(getIDInfo("identification info")); + headers.addAll(getMapDataSrc("shape data source")); + headers.addAll(getProcInfo("processing information")); + headers.addAll(getMapInfo("map specific information")); + headers.addAll(getSpatialInfo("summary spatial information")); - // add UX_X,Y,Z - headers.addAll(getUX()); - - // add UY_X,Y,Z - headers.addAll(getUY()); - - // add UZ_X,Y,Z - headers.addAll(getUZ()); - - return headers; - } - - /** - * Return the HeaderCards associated with a specific product. By default we use the ALTWG specific - * product keywords - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getSpecificInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headers; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIGMA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DSIG_DEF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DENSITY)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ROT_RATE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.REF_POT)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MAJ)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MIN)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_PA)); + /** + * return Fits header block that contains information about the fits header itself. + * + * @return + * @throws HeaderCardException + */ + public List getHeaderInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - /** - * Return headercards associated with the upper/lower left/right corners of the image. - * - * @return - * @throws HeaderCardException - */ - public List getCornerCards() throws HeaderCardException { + return headers; + } - List headers = new ArrayList(); - String fmtS = "%18.13f"; - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT, fmtS)); + /** + * Fits header block containing information about the mission. + * + * @return + * @throws HeaderCardException + */ + public List getMissionInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); - /** - * Return headercards for vector to center of image. - * - * @return - * @throws HeaderCardException - */ - public List getCenterVec() throws HeaderCardException { + return headers; + } - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Z)); + /** + * Fits header block containing observation or ID related information. + * + * @return + * @throws HeaderCardException + */ + public List getIDInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - public List getUX() throws HeaderCardException { + return headers; + } - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Z)); + /** + * Fits header block containing information about the source data used to create the map. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getMapDataSrc(String comment) throws HeaderCardException { - return headers; + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.CREATOR)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); + return headers; + } - } + public List getMapInfo(String comment) throws HeaderCardException { - public List getUY() throws HeaderCardException { + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSDI)); - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Z)); + return headers; + } - return headers; + /** + * Fits header block containing information about the software processing done to generate the + * product. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getProcInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - public List getUZ() throws HeaderCardException { + return headers; + } - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Z)); + public List getSpatialInfo(String comment) throws HeaderCardException { - return headers; + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } - } + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + headers.addAll(getCornerCards()); + + // add CNTR_V_X,Y,Z + headers.addAll(getCenterVec()); + + // add UX_X,Y,Z + headers.addAll(getUX()); + + // add UY_X,Y,Z + headers.addAll(getUY()); + + // add UZ_X,Y,Z + headers.addAll(getUZ()); + + return headers; + } + + /** + * Return the HeaderCards associated with a specific product. By default we use the ALTWG specific + * product keywords + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getSpecificInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIGMA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DSIG_DEF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DENSITY)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ROT_RATE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.REF_POT)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MAJ)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_MIN)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TILT_PA)); + + return headers; + } + + /** + * Return headercards associated with the upper/lower left/right corners of the image. + * + * @return + * @throws HeaderCardException + */ + public List getCornerCards() throws HeaderCardException { + + List headers = new ArrayList(); + String fmtS = "%18.13f"; + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT, fmtS)); + + return headers; + } + + /** + * Return headercards for vector to center of image. + * + * @return + * @throws HeaderCardException + */ + public List getCenterVec() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Z)); + + return headers; + } + + public List getUX() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Z)); + + return headers; + } + + public List getUY() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Z)); + + return headers; + } + + public List getUZ() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Z)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/DTMFits.java b/src/main/java/terrasaur/fits/DTMFits.java index e8a0446..84366f2 100644 --- a/src/main/java/terrasaur/fits/DTMFits.java +++ b/src/main/java/terrasaur/fits/DTMFits.java @@ -32,342 +32,332 @@ import terrasaur.enums.FitsHeaderType; * Abstract generic class with concrete methods and attributes for creating a FITS DTM cube with a * generalized fits header. Specific implementations can be written to create custom fits headers as * needed. - * + * * @author espirrc1 * */ public abstract class DTMFits { - public final String COMMENT = "COMMENT"; - final FitsHdr fitsHdr; - private FitsData fitsData; - private boolean dataContained = false; - public final FitsHeaderType fitsHeaderType; + public final String COMMENT = "COMMENT"; + final FitsHdr fitsHdr; + private FitsData fitsData; + private boolean dataContained = false; + public final FitsHeaderType fitsHeaderType; - public DTMFits(FitsHdr fitsHdr, FitsHeaderType fitsHeaderType) { - this.fitsHdr = fitsHdr; - this.fitsHeaderType = fitsHeaderType; - } - - public void setData(FitsData fitsData) { - this.fitsData = fitsData; - dataContained = true; - } - - public List createFitsHeader(List planeList) throws HeaderCardException { - - List headers = new ArrayList(); - - headers.addAll(getHeaderInfo("header information")); - headers.addAll(getMissionInfo("mission information")); - headers.addAll(getIDInfo("identification info")); - headers.addAll(getMapDataSrc("data source")); - headers.addAll(getProcInfo("processing information")); - headers.addAll(getMapInfo("map specific information")); - headers.addAll(getSpatialInfo("summary spatial information")); - headers.addAll(getPlaneInfo("plane information", planeList)); - headers.addAll(getSpecificInfo("product specific")); - - // end keyword - headers.add(getEnd()); - - return headers; - } - - /** - * return Fits header block that contains information about the fits header itself. No string - * passed, so no comment in header. - * - * @return - * @throws HeaderCardException - */ - public List getHeaderInfo() throws HeaderCardException { - return getHeaderInfo(""); - } - - /** - * return Fits header block that contains information about the fits header itself. This is a - * custom section and so is left empty here. It can be defined in the concrete classes that extend - * this class. - * - * @return - * @throws HeaderCardException - */ - public List getHeaderInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - return headers; - - } - - /** - * Fits header block containing information about the mission. - * - * @return - * @throws HeaderCardException - */ - public List getMissionInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); - - return headers; - - } - - /** - * Fits header block containing observation or ID related information. - * - * @return - * @throws HeaderCardException - */ - public List getIDInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - - return headers; - - } - - /** - * Fits header block containing information about the source data used to create the map. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getMapDataSrc(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); - - return headers; - - } - - public List getMapInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - - return headers; - } - - /** - * Fits header block containing information about the software processing done to generate the - * product. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getProcInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - - return headers; - - } - - /** - * Creates header block containing spatial information for the DTM, e.g. corner locations, vector - * to center, Ux, Uy, Uz. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getSpatialInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public DTMFits(FitsHdr fitsHdr, FitsHeaderType fitsHeaderType) { + this.fitsHdr = fitsHdr; + this.fitsHeaderType = fitsHeaderType; } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); - headers.addAll(getCornerCards()); - - // remove these keywords. They are specific to local and MLNs - // headers.addAll(getCenterVec()); - // headers.addAll(getUX()); - // headers.addAll(getUY()); - // headers.addAll(getUZ()); - - return headers; - } - - /** - * Return the HeaderCards describing each DTM plane. Used to build the portion of the fits header - * that contains information about the planes in the DTM cube. Checks to see that all data planes - * are described by comparing size of planeList against length of fits data. - * - * @param comment - * @param planeList - * @return - * @throws HeaderCardException - */ - public List getPlaneInfo(String comment, List planeList) - throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.addAll(planeList); - - if (!dataContained) { - String errMesg = "ERROR! Cannot return keywords describing the DTM cube without " - + "having the actual data!"; - throw new RuntimeException(errMesg); + public void setData(FitsData fitsData) { + this.fitsData = fitsData; + dataContained = true; } - // check if planeList describes all the planes in data, throw runtime exception if not. - if (planeList.size() != fitsData.getData().length) { - System.out.println("Error: plane List has " + planeList.size() + " planes but datacube has " - + fitsData.getData().length + " planes"); - for (HeaderCard thisPlane : planeList) { - System.out.println(thisPlane.getKey() + ":" + thisPlane.getValue()); - } - String errMesg = "Error: plane List has " + planeList.size() + " planes but datacube " - + " has " + fitsData.getData().length + " planes"; - throw new RuntimeException(errMesg); + public List createFitsHeader(List planeList) throws HeaderCardException { + + List headers = new ArrayList(); + + headers.addAll(getHeaderInfo("header information")); + headers.addAll(getMissionInfo("mission information")); + headers.addAll(getIDInfo("identification info")); + headers.addAll(getMapDataSrc("data source")); + headers.addAll(getProcInfo("processing information")); + headers.addAll(getMapInfo("map specific information")); + headers.addAll(getSpatialInfo("summary spatial information")); + headers.addAll(getPlaneInfo("plane information", planeList)); + headers.addAll(getSpecificInfo("product specific")); + + // end keyword + headers.add(getEnd()); + + return headers; } - return headers; - } - - /** - * Return the HeaderCards associated with a specific product. - * - * @param comment - * @return - * @throws HeaderCardException - */ - public List getSpecificInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + /** + * return Fits header block that contains information about the fits header itself. No string + * passed, so no comment in header. + * + * @return + * @throws HeaderCardException + */ + public List getHeaderInfo() throws HeaderCardException { + return getHeaderInfo(""); } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.PXPERDEG)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DENSITY)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.ROT_RATE)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.REF_POT)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MAJ)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MIN)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_PA)); + /** + * return Fits header block that contains information about the fits header itself. This is a + * custom section and so is left empty here. It can be defined in the concrete classes that extend + * this class. + * + * @return + * @throws HeaderCardException + */ + public List getHeaderInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + return headers; + } - /** - * Return headercards associated with the upper/lower left/right corners of the image. - * - * @return - * @throws HeaderCardException - */ - public List getCornerCards() throws HeaderCardException { + /** + * Fits header block containing information about the mission. + * + * @return + * @throws HeaderCardException + */ + public List getMissionInfo(String comment) throws HeaderCardException { - List headers = new ArrayList(); - String fmtS = "%18.13f"; - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG, fmtS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT, fmtS)); + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); - return headers; - } + return headers; + } - /** - * Return headercards for vector to center of image. - * - * @return - * @throws HeaderCardException - */ - public List getCenterVec() throws HeaderCardException { + /** + * Fits header block containing observation or ID related information. + * + * @return + * @throws HeaderCardException + */ + public List getIDInfo(String comment) throws HeaderCardException { - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Z)); + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - return headers; - } + return headers; + } - public List getUX() throws HeaderCardException { + /** + * Fits header block containing information about the source data used to create the map. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getMapDataSrc(String comment) throws HeaderCardException { - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Z)); + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); - return headers; + return headers; + } - } + public List getMapInfo(String comment) throws HeaderCardException { - public List getUY() throws HeaderCardException { + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Z)); + return headers; + } - return headers; + /** + * Fits header block containing information about the software processing done to generate the + * product. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getProcInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - public List getUZ() throws HeaderCardException { + return headers; + } - List headers = new ArrayList(); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_X)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Y)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Z)); + /** + * Creates header block containing spatial information for the DTM, e.g. corner locations, vector + * to center, Ux, Uy, Uz. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getSpatialInfo(String comment) throws HeaderCardException { - return headers; + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } - } + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + headers.addAll(getCornerCards()); - public HeaderCard getEnd() throws HeaderCardException { - return new HeaderCard(HeaderTag.END.toString(), HeaderTag.END.value(), HeaderTag.END.comment()); - } + // remove these keywords. They are specific to local and MLNs + // headers.addAll(getCenterVec()); + // headers.addAll(getUX()); + // headers.addAll(getUY()); + // headers.addAll(getUZ()); + return headers; + } + + /** + * Return the HeaderCards describing each DTM plane. Used to build the portion of the fits header + * that contains information about the planes in the DTM cube. Checks to see that all data planes + * are described by comparing size of planeList against length of fits data. + * + * @param comment + * @param planeList + * @return + * @throws HeaderCardException + */ + public List getPlaneInfo(String comment, List planeList) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.addAll(planeList); + + if (!dataContained) { + String errMesg = + "ERROR! Cannot return keywords describing the DTM cube without " + "having the actual data!"; + throw new RuntimeException(errMesg); + } + + // check if planeList describes all the planes in data, throw runtime exception if not. + if (planeList.size() != fitsData.getData().length) { + System.out.println("Error: plane List has " + planeList.size() + " planes but datacube has " + + fitsData.getData().length + " planes"); + for (HeaderCard thisPlane : planeList) { + System.out.println(thisPlane.getKey() + ":" + thisPlane.getValue()); + } + String errMesg = "Error: plane List has " + planeList.size() + " planes but datacube " + " has " + + fitsData.getData().length + " planes"; + throw new RuntimeException(errMesg); + } + + return headers; + } + + /** + * Return the HeaderCards associated with a specific product. + * + * @param comment + * @return + * @throws HeaderCardException + */ + public List getSpecificInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + + headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.PXPERDEG)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DENSITY)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.ROT_RATE)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.REF_POT)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MAJ)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_MIN)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.TILT_PA)); + + return headers; + } + + /** + * Return headercards associated with the upper/lower left/right corners of the image. + * + * @return + * @throws HeaderCardException + */ + public List getCornerCards() throws HeaderCardException { + + List headers = new ArrayList(); + String fmtS = "%18.13f"; + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG, fmtS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT, fmtS)); + + return headers; + } + + /** + * Return headercards for vector to center of image. + * + * @return + * @throws HeaderCardException + */ + public List getCenterVec() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CNTR_V_Z)); + + return headers; + } + + public List getUX() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UX_Z)); + + return headers; + } + + public List getUY() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UY_Z)); + + return headers; + } + + public List getUZ() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_X)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Y)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.UZ_Z)); + + return headers; + } + + public HeaderCard getEnd() throws HeaderCardException { + return new HeaderCard(HeaderTag.END.toString(), HeaderTag.END.value(), HeaderTag.END.comment()); + } } diff --git a/src/main/java/terrasaur/fits/FitsData.java b/src/main/java/terrasaur/fits/FitsData.java index d4b5dab..5175752 100644 --- a/src/main/java/terrasaur/fits/FitsData.java +++ b/src/main/java/terrasaur/fits/FitsData.java @@ -27,186 +27,184 @@ import terrasaur.enums.SrcProductType; public class FitsData { - private final double[][][] data; - private final double[] V; - private final double[] ux; - private final double[] uy; - private final double[] uz; - private final double scale; - private final double gsd; - private final boolean hasV; - private final boolean hasUnitv; - private final boolean hasGsd; - private final boolean isGlobal; - private final boolean hasAltType; - private final AltwgDataType altProd; - private final String dataSource; - - private FitsData(FitsDataBuilder b) { - this.data = b.data; - this.V = b.V; - this.ux = b.ux; - this.uy = b.uy; - this.uz = b.uz; - this.scale = b.scale; - this.gsd = b.gsd; - this.hasV = b.hasV; - this.hasUnitv = b.hasUnitv; - this.hasGsd = b.hasGsd; - this.hasAltType = b.hasAltType; - this.isGlobal = b.isGlobal; - this.altProd = b.altProd; - this.dataSource = b.dataSource; - } - - public AltwgDataType getAltProdType() { - return this.altProd; - } - - public String getSrcProdType() { - return this.dataSource; - } - - public double[][][] getData() { - return this.data; - } - - public boolean hasV() { - return this.hasV; - } - - public double[] getV() { - return this.V; - } - - public boolean hasUnitv() { - return this.hasUnitv; - } - - public double[] getUnit(UnitDir udir) { - switch (udir) { - case UX: - return this.ux; - - case UY: - return this.uy; - - case UZ: - return this.uz; - - default: - throw new RuntimeException(); - } - } - - public double getScale() { - return this.scale; - } - - public boolean hasGsd() { - return this.hasGsd; - } - - public boolean hasAltType() { - return this.hasAltType; - } - - public boolean isGlobal() { - return this.isGlobal; - } - - public double getGSD() { - if (this.hasGsd) { - return this.gsd; - } else { - String errMesg = "ERROR! fitsData does not have gsd!"; - throw new RuntimeException(errMesg); - } - } - - - public static class FitsDataBuilder { private final double[][][] data; - private double[] V = null; - private double[] ux = null; - private double[] uy = null; - private double[] uz = null; - private boolean hasV = false; - private boolean hasUnitv = false; - private boolean hasGsd = false; - private boolean isGlobal = false; - private boolean hasAltType = false; - private double scale = Double.NaN; - private double gsd = Double.NaN; - private AltwgDataType altProd = null; - private String dataSource = SrcProductType.UNKNOWN.toString(); + private final double[] V; + private final double[] ux; + private final double[] uy; + private final double[] uz; + private final double scale; + private final double gsd; + private final boolean hasV; + private final boolean hasUnitv; + private final boolean hasGsd; + private final boolean isGlobal; + private final boolean hasAltType; + private final AltwgDataType altProd; + private final String dataSource; - /** - * Constructor. isGlobal used to fill out fits keyword describing whether data is local or - * global. May also be used for fits naming convention. - * - * @param data - * @param isGlobal - */ - public FitsDataBuilder(double[][][] data, boolean isGlobal) { - this.data = data; - this.isGlobal = isGlobal; + private FitsData(FitsDataBuilder b) { + this.data = b.data; + this.V = b.V; + this.ux = b.ux; + this.uy = b.uy; + this.uz = b.uz; + this.scale = b.scale; + this.gsd = b.gsd; + this.hasV = b.hasV; + this.hasUnitv = b.hasUnitv; + this.hasGsd = b.hasGsd; + this.hasAltType = b.hasAltType; + this.isGlobal = b.isGlobal; + this.altProd = b.altProd; + this.dataSource = b.dataSource; } - public FitsDataBuilder setAltProdType(AltwgDataType altProd) { - this.altProd = altProd; - this.hasAltType = true; - return this; + public AltwgDataType getAltProdType() { + return this.altProd; } - public FitsDataBuilder setDataSource(String dataSource) { - this.dataSource = dataSource; - return this; + public String getSrcProdType() { + return this.dataSource; } - public FitsDataBuilder setV(double[] V) { - this.V = V; - this.hasV = true; - return this; + public double[][][] getData() { + return this.data; } - public FitsDataBuilder setU(double[] uvec, UnitDir udir) { - switch (udir) { - case UX: - this.ux = uvec; - this.hasUnitv = true; - break; - - case UY: - this.uy = uvec; - this.hasUnitv = true; - break; - - case UZ: - this.uz = uvec; - this.hasUnitv = true; - break; - - default: - throw new RuntimeException(); - - } - return this; + public boolean hasV() { + return this.hasV; } - public FitsDataBuilder setScale(double scale) { - this.scale = scale; - return this; + public double[] getV() { + return this.V; } - public FitsDataBuilder setGSD(double gsd) { - this.gsd = gsd; - this.hasGsd = true; - return this; + public boolean hasUnitv() { + return this.hasUnitv; } - public FitsData build() { - return new FitsData(this); + public double[] getUnit(UnitDir udir) { + switch (udir) { + case UX: + return this.ux; + + case UY: + return this.uy; + + case UZ: + return this.uz; + + default: + throw new RuntimeException(); + } + } + + public double getScale() { + return this.scale; + } + + public boolean hasGsd() { + return this.hasGsd; + } + + public boolean hasAltType() { + return this.hasAltType; + } + + public boolean isGlobal() { + return this.isGlobal; + } + + public double getGSD() { + if (this.hasGsd) { + return this.gsd; + } else { + String errMesg = "ERROR! fitsData does not have gsd!"; + throw new RuntimeException(errMesg); + } + } + + public static class FitsDataBuilder { + private final double[][][] data; + private double[] V = null; + private double[] ux = null; + private double[] uy = null; + private double[] uz = null; + private boolean hasV = false; + private boolean hasUnitv = false; + private boolean hasGsd = false; + private boolean isGlobal = false; + private boolean hasAltType = false; + private double scale = Double.NaN; + private double gsd = Double.NaN; + private AltwgDataType altProd = null; + private String dataSource = SrcProductType.UNKNOWN.toString(); + + /** + * Constructor. isGlobal used to fill out fits keyword describing whether data is local or + * global. May also be used for fits naming convention. + * + * @param data + * @param isGlobal + */ + public FitsDataBuilder(double[][][] data, boolean isGlobal) { + this.data = data; + this.isGlobal = isGlobal; + } + + public FitsDataBuilder setAltProdType(AltwgDataType altProd) { + this.altProd = altProd; + this.hasAltType = true; + return this; + } + + public FitsDataBuilder setDataSource(String dataSource) { + this.dataSource = dataSource; + return this; + } + + public FitsDataBuilder setV(double[] V) { + this.V = V; + this.hasV = true; + return this; + } + + public FitsDataBuilder setU(double[] uvec, UnitDir udir) { + switch (udir) { + case UX: + this.ux = uvec; + this.hasUnitv = true; + break; + + case UY: + this.uy = uvec; + this.hasUnitv = true; + break; + + case UZ: + this.uz = uvec; + this.hasUnitv = true; + break; + + default: + throw new RuntimeException(); + } + return this; + } + + public FitsDataBuilder setScale(double scale) { + this.scale = scale; + return this; + } + + public FitsDataBuilder setGSD(double gsd) { + this.gsd = gsd; + this.hasGsd = true; + return this; + } + + public FitsData build() { + return new FitsData(this); + } } - } } diff --git a/src/main/java/terrasaur/fits/FitsHdr.java b/src/main/java/terrasaur/fits/FitsHdr.java index b875036..a329e32 100644 --- a/src/main/java/terrasaur/fits/FitsHdr.java +++ b/src/main/java/terrasaur/fits/FitsHdr.java @@ -33,12 +33,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; -import org.apache.commons.io.FileUtils; import nom.tam.fits.FitsException; import nom.tam.fits.Header; import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; import nom.tam.util.Cursor; +import org.apache.commons.io.FileUtils; import picante.math.coords.CoordConverters; import picante.math.coords.LatitudinalVector; import picante.math.vectorspace.UnwritableVectorIJK; @@ -52,1091 +52,1045 @@ import picante.math.vectorspace.UnwritableVectorIJK; */ public class FitsHdr { - public final Map fitsKV; - - private FitsHdr(FitsHdrBuilder b) { - fitsKV = b.getMap(); - } - - /** - * Return the map as a list. - * - * @return - */ - public List mapToList() { - List headerList = new ArrayList(); - - for (String thisKey : fitsKV.keySet()) { - headerList.add(fitsKV.get(thisKey)); - } - - return headerList; - } - - public static class FitsHdrBuilder { - private Map fitsKV = new LinkedHashMap(); - - // change this to allow code to print out warnings - private boolean printWarnings = false; - - /** - * basic no-arg constructor. Note that this constructor does NOT populate the fitsKV map! If you - * want an initial builder with initialized map then call initHdrBuilder(). - */ - public FitsHdrBuilder() { + public final Map fitsKV; + private FitsHdr(FitsHdrBuilder b) { + fitsKV = b.getMap(); } /** - * Return all HeaderCards in their current state - * + * Return the map as a list. + * * @return */ - public List getHeaderCards() { - List headers = new ArrayList(); - for (String keyword : fitsKV.keySet()) { - headers.add(fitsKV.get(keyword)); - } - return headers; + public List mapToList() { + List headerList = new ArrayList(); + + for (String thisKey : fitsKV.keySet()) { + headerList.add(fitsKV.get(thisKey)); + } + + return headerList; } - /** - * Put HeaderCard into map with keyword String. Updates the Headercard if record already exists - * in the map. HeaderCard. - * - * @param keyword - * @param hdrCard - * @return - */ - // public FitsHdrBuilder putCard(String keyword, HeaderCard hdrCard) { - // this.fitsKV.put(keyword, hdrCard); - // return this; - // } + public static class FitsHdrBuilder { + private Map fitsKV = new LinkedHashMap(); - // public FitsHdrBuilder putCard(HeaderTag hdrTag, HeaderCard hdrCard) { - // putCard(hdrTag.toString(), hdrCard); - // return this; - // } + // change this to allow code to print out warnings + private boolean printWarnings = false; + + /** + * basic no-arg constructor. Note that this constructor does NOT populate the fitsKV map! If you + * want an initial builder with initialized map then call initHdrBuilder(). + */ + public FitsHdrBuilder() {} + + /** + * Return all HeaderCards in their current state + * + * @return + */ + public List getHeaderCards() { + List headers = new ArrayList(); + for (String keyword : fitsKV.keySet()) { + headers.add(fitsKV.get(keyword)); + } + return headers; + } + + /** + * Put HeaderCard into map with keyword String. Updates the Headercard if record already exists + * in the map. HeaderCard. + * + * @param keyword + * @param hdrCard + * @return + */ + // public FitsHdrBuilder putCard(String keyword, HeaderCard hdrCard) { + // this.fitsKV.put(keyword, hdrCard); + // return this; + // } + + // public FitsHdrBuilder putCard(HeaderTag hdrTag, HeaderCard hdrCard) { + // putCard(hdrTag.toString(), hdrCard); + // return this; + // } + + /** + * Set the HeaderCards for HeaderTags associated with the corners of the data. Set value = "N/A" + * if input data is null. Will use the same longitude range for corner points as in the data. + * I.e. if the data uses -180 to 180 then will use -180 to 180 range for longitude corner + * points. If data uses 0 - 360 then will use 0-360 for longitude corner points. + * + * @param data + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setCornerCards(double[][][] data) throws HeaderCardException { + if (data != null) { + + System.out.println("Data not null"); + int height = data[0].length; + int width = data[0][0].length; + + double llclat = data[0][0][0]; + double llclng = data[1][0][0]; + double urclat = data[0][height - 1][width - 1]; + double urclng = data[1][height - 1][width - 1]; + double lrclat = data[0][0][width - 1]; + double lrclng = data[1][0][width - 1]; + double ulclat = data[0][height - 1][0]; + double ulclng = data[1][height - 1][0]; + setVCbyHeaderTag(HeaderTag.LLCLNG, llclng, HeaderTag.LLCLNG.comment()); + setVCbyHeaderTag(HeaderTag.LLCLAT, llclat, HeaderTag.LLCLAT.comment()); + setVCbyHeaderTag(HeaderTag.URCLNG, urclng, HeaderTag.URCLNG.comment()); + setVCbyHeaderTag(HeaderTag.URCLAT, urclat, HeaderTag.URCLAT.comment()); + setVCbyHeaderTag(HeaderTag.LRCLNG, lrclng, HeaderTag.LRCLNG.comment()); + setVCbyHeaderTag(HeaderTag.LRCLAT, lrclat, HeaderTag.LRCLAT.comment()); + setVCbyHeaderTag(HeaderTag.ULCLNG, ulclng, HeaderTag.ULCLNG.comment()); + setVCbyHeaderTag(HeaderTag.ULCLAT, ulclat, HeaderTag.ULCLAT.comment()); + + } else { + + System.out.println("Data is null"); + + setVCbyHeaderTag(HeaderTag.LLCLNG, "N/A", HeaderTag.LLCLNG.comment()); + setVCbyHeaderTag(HeaderTag.LLCLAT, "N/A", HeaderTag.LLCLAT.comment()); + setVCbyHeaderTag(HeaderTag.URCLNG, "N/A", HeaderTag.URCLNG.comment()); + setVCbyHeaderTag(HeaderTag.URCLAT, "N/A", HeaderTag.URCLAT.comment()); + setVCbyHeaderTag(HeaderTag.LRCLNG, "N/A", HeaderTag.LRCLNG.comment()); + setVCbyHeaderTag(HeaderTag.LRCLAT, "N/A", HeaderTag.LRCLAT.comment()); + setVCbyHeaderTag(HeaderTag.ULCLNG, "N/A", HeaderTag.ULCLNG.comment()); + setVCbyHeaderTag(HeaderTag.ULCLAT, "N/A", HeaderTag.ULCLAT.comment()); + } + + return this; + } + + /** + * Set the HeaderCards for HeaderTags associated with the center latitude, longitude + * + * @param llr + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setCenterLatLon(LatitudinalVector llr) throws HeaderCardException { + if (llr != null) { + setVCbyHeaderTag(HeaderTag.CLON, Math.toDegrees(llr.getLongitude()), HeaderTag.CLON.comment()); + setVCbyHeaderTag(HeaderTag.CLAT, Math.toDegrees(llr.getLatitude()), HeaderTag.CLAT.comment()); + } + return this; + } + + /** + * Set the HeaderCards for HeaderTags associated with center latitude, longitude where center + * lat,lon is defined by the vector 'V'. If 'V' is null then uses default global lat,lon if + * 'isGlobal', otherwise returns with lat,lon headercards set to "N/A"; + * + * @param V + * @param isGlobal + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setCenterLatLon(double[] V, boolean isGlobal) throws HeaderCardException { + if (V != null) { + LatitudinalVector llr = CoordConverters.convertToLatitudinal(new UnwritableVectorIJK(V)); + setCenterLatLon(llr); + + } else { + + if (isGlobal) { + LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); + setCenterLatLon(llr); + } else { + + // unknown. Don't know why we wouldn't know the center lat, lon + setVCbyHeaderTag(HeaderTag.CLON, "N/A", HeaderTag.CLON.comment()); + setVCbyHeaderTag(HeaderTag.CLAT, "N/A", HeaderTag.CLAT.comment()); + } + } + return this; + } + + /** + * Hardcode center lat, lon to be 0, 0. Do this only if you know this is a global fits file. + * + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setGlobalCenter() throws HeaderCardException { + LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); + setCenterLatLon(llr); + return this; + } + + /** + * Set HeaderCards for HeaderTags associated with the vector to center of image. Throws + * RuntimeException if V is null. + * + * @param V + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setCenterVec(double[] V) throws HeaderCardException { + if (V != null) { + setVCbyHeaderTag(HeaderTag.CNTR_V_X, V[0], HeaderTag.CNTR_V_X.comment()); + setVCbyHeaderTag(HeaderTag.CNTR_V_Y, V[1], HeaderTag.CNTR_V_Y.comment()); + setVCbyHeaderTag(HeaderTag.CNTR_V_Z, V[2], HeaderTag.CNTR_V_Z.comment()); + } else { + + String errMesg = "ERROR! Center Vector is null! It could be that center" + + " vector information is already contained in FitsHdrBuilder!"; + throw new RuntimeException(errMesg); + } + return this; + } + + /** + * Use the current time when this method is called to set the date when product was produced + * + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setDateprod() throws HeaderCardException { + + // DateFormat dateFormat = new + // SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssssZ"); + // date-time terminates in 'Z' + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date date = new Date(); + setVbyHeaderTag(HeaderTag.DATEPRD, dateFormat.format(date)); + + return this; + } + + /** + * Set HeaderCards associated with unit vector Ux defining the reference frame + * + * @param unitV + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setUx(double[] unitV) throws HeaderCardException { + + if (unitV != null) { + setVCbyHeaderTag(HeaderTag.UX_X, unitV[0], null); + setVCbyHeaderTag(HeaderTag.UX_Y, unitV[1], null); + setVCbyHeaderTag(HeaderTag.UX_Z, unitV[2], null); + + } else { + String errMesg = "ERROR! UX Vector is null! It could be that UX" + + " vector information is already contained in FitsHdrBuilder!"; + throw new RuntimeException(errMesg); + } + + return this; + } + + /** + * Set HeaderCards associated with unit vector Uy defining the reference frame + * + * @param unitV + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setUy(double[] unitV) throws HeaderCardException { + + if (unitV != null) { + + setVCbyHeaderTag(HeaderTag.UY_X, unitV[0], null); + setVCbyHeaderTag(HeaderTag.UY_Y, unitV[1], null); + setVCbyHeaderTag(HeaderTag.UY_Z, unitV[2], null); + } else { + String errMesg = "ERROR! UY Vector is null! It could be that UY" + + " vector information is already contained in FitsHdrBuilder!"; + throw new RuntimeException(errMesg); + } + + return this; + } + + /** + * Set HeaderCards associated with unit vector Uz defining the reference frame + * + * @param unitV + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setUz(double[] unitV) throws HeaderCardException { + + if (unitV != null) { + setVCbyHeaderTag(HeaderTag.UZ_X, unitV[0], null); + setVCbyHeaderTag(HeaderTag.UZ_Y, unitV[1], null); + setVCbyHeaderTag(HeaderTag.UZ_Z, unitV[2], null); + + } else { + String errMesg = "ERROR! UZ Vector is null! It could be that UZ" + + " vector information is already contained in FitsHdrBuilder!"; + throw new RuntimeException(errMesg); + } + + return this; + } + + /** + * Set the GSD and GSDI values based on input ground sample distance. + * + * @param gsd + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setGSD(double gsd) throws HeaderCardException { + + // determine if GSD is close to an integer value + long gsdRound = Math.round(gsd); + double gsdRounded = gsdRound; + System.out.println("gsd calculated to be:" + String.valueOf(gsdRounded)); + + if (Math.abs(gsd - gsdRounded) < 1.0e-5) { + gsd = gsdRounded; + } + + setVCbyHeaderTag(HeaderTag.GSD, gsd, HeaderTag.GSD.comment()); + + // use default units for GSDI. Round to nearest integer + String gsdiUnits = HeaderTag.GSDI.comment(); + gsdiUnits = gsdiUnits.replace("[unk]", "[mm]"); + setVCbyHeaderTag(HeaderTag.GSDI, gsdRounded, gsdiUnits); + + return this; + } + + /** + * Change the string value for a HeaderCard in the map. If it doesn't already exist then create + * a new HeaderCard. + * + * @param hdrTag + * @param value + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setVbyHeaderTag(HeaderTag hdrTag, String value) throws HeaderCardException { + if (fitsKV.containsKey(hdrTag.toString())) { + + fitsKV.get(hdrTag.toString()).setValue(value); + + } else { + + if (printWarnings) { + System.out.println("WARNING! header:" + hdrTag.toString() + " is new. Will add it to " + + "fits header builder."); + } + + HeaderCard newCard = new HeaderCard(hdrTag.toString(), value, hdrTag.comment()); + fitsKV.put(hdrTag.toString(), newCard); + } + return this; + } + + /** + * Change the value for a HeaderCard in the map + * + * @param hdrTag + * @param value + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setVbyHeaderTag(HeaderTag hdrTag, double value) throws HeaderCardException { + + String hdrKey = hdrTag.toString(); + if (fitsKV.containsKey(hdrKey)) { + + fitsKV.get(hdrKey).setValue(value); + + } else { + if (printWarnings) { + System.out.println( + "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); + } + HeaderCard newCard = new HeaderCard(hdrKey, value, hdrTag.comment()); + fitsKV.put(hdrTag.toString(), newCard); + } + + return this; + } + + /** + * Change the string value and comment for a HeaderCard in the map. HeaderCard stores value as a + * string and adds quotes around it in the fits header to indicate that it is a 'string' value + * + * @param hdrTag + * @param value + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setVCbyHeaderTag(HeaderTag hdrTag, String value, String comment) + throws HeaderCardException { + + String hdrKey = hdrTag.toString(); + if (fitsKV.containsKey(hdrKey)) { + fitsKV.get(hdrKey).setValue(value); + fitsKV.get(hdrKey).setComment(comment); + + } else { + if (printWarnings) { + System.out.println( + "WARNING! header:" + hdrTag + " is new. Will add it to " + "fits header builder."); + } + HeaderCard newCard = new HeaderCard(hdrKey, value, comment); + fitsKV.put(hdrKey, newCard); + } + return this; + } + + /** + * Change the comment for a given HeaderCard in the map. Leaves the value unchanged + * + * @param hdrTag + * @param comment + * @return + */ + public FitsHdrBuilder setCbyHeaderTag(HeaderTag hdrTag, String comment) { + String hdrkey = hdrTag.toString(); + if (fitsKV.containsKey(hdrkey)) { + + fitsKV.get(hdrkey).setComment(comment); + + } else { + String errMesg = "ERROR! HeaderTag:" + hdrTag + " does not exist in FitsHdrBuilder!"; + throw new RuntimeException(errMesg); + } + return this; + } + + /** + * Change the double value and comment for a HeaderCard in the map. HeaderCard stores value as a + * string but does NOT add quotes around it in the fits header. + * + * @param hdrTag + * @param value + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setVCbyHeaderTag(HeaderTag hdrTag, double value, String comment) + throws HeaderCardException { + + String hdrKey = hdrTag.toString(); + if (fitsKV.containsKey(hdrKey)) { + + fitsKV.get(hdrKey).setValue(value); + fitsKV.get(hdrKey).setComment(comment); + + } else { + if (printWarnings) { + System.out.println( + "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); + } + HeaderCard newCard = new HeaderCard(hdrKey, value, comment); + fitsKV.put(hdrKey, newCard); + } + return this; + } + + /** + * Update existing HeaderCard with input headercard. If it doesn't exist then add the headercard + * to the map. + * + * @param headerCard + * @return + */ + public FitsHdrBuilder setbyHeaderCard(HeaderCard headerCard) { + String hdrKey = headerCard.getKey(); + + if (!fitsKV.containsKey(hdrKey)) { + if (printWarnings) { + System.out.println( + "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); + } + } + + fitsKV.put(hdrKey, headerCard); + return this; + } + + /** + * Convenience method for updating all possible keywords using information found in FitsData. + * + * @param fitsData + * @return + * @throws HeaderCardException + */ + public FitsHdrBuilder setByFitsData(FitsData fitsData) throws HeaderCardException { + + HeaderCard hdrCard; + + // set map type, global or local + String hdrKey = HeaderTag.MAP_TYPE.toString(); + String mapType = "global"; + if (!fitsData.isGlobal()) { + mapType = "local"; + } + if (fitsKV.containsKey(hdrKey)) { + fitsKV.get(hdrKey).setValue(mapType); + } else { + + // fitsKV is no longer initially populated, so just add this. + hdrCard = new HeaderCard(hdrKey, mapType, HeaderTag.MAP_TYPE.comment()); + fitsKV.put(hdrKey, hdrCard); + } + + this.setCornerCards(fitsData.getData()); + + /* + * extract map information from fitsData if present: vector to center: V unit vectors + * describing reference frame: UX, UY, UZ, gsd + */ + if (fitsData.hasV()) { + this.setCenterVec(fitsData.getV()).setCenterLatLon(fitsData.getV(), fitsData.isGlobal()); + } else { + + // no center vector in fitsData. Check if this is a global product. + if (fitsData.isGlobal()) { + + // set center lat, lon to be 0deg, 0deg + this.setGlobalCenter(); + } + + // for the case where there is no center vector in fitsData + // and it is NOT a global product, assume the info is already captured + // in fitsKV. + } + + if (fitsData.hasUnitv()) { + // assume all unit vectors are present + this.setUx(fitsData.getUnit(UnitDir.UX)) + .setUy(fitsData.getUnit(UnitDir.UY)) + .setUz(fitsData.getUnit(UnitDir.UZ)); + } + + if (fitsData.hasGsd()) { + this.setGSD(fitsData.getGSD()); + } + + return this; + } + + private Map getMap() { + return fitsKV; + } + + /** + * Check whether map contains the String keyword. + * + * @return + */ + public boolean containsKey(String keyWord) { + + return fitsKV.containsKey(keyWord); + } + + public boolean containsKey(HeaderTag keyWord) { + return fitsKV.containsKey(keyWord.toString()); + } + + /** + * Search map and return the HeaderCard associated with the String keyword. Returns null if key + * not contained in map. + * + * @param keyWord + * @return + */ + public HeaderCard getCard(String keyWord) { + + return fitsKV.get(keyWord); + } + + public HeaderCard getCard(HeaderTag keyWord) { + return fitsKV.get(keyWord.toString()); + } + + /** + * Add default values for the header tag to fitsKV map. The method checks to see if the + * headertag exists in the map. If it does exist then it will not modify the headercard! + * + * @param headerTag + * @throws HeaderCardException + */ + public void addCard(HeaderTag headerTag) throws HeaderCardException { + + if (this.containsKey(headerTag)) { + System.out.println("WARNING: keyword:" + headerTag.toString() + " already exists in map. Will not" + + " add a default headercard!"); + } else { + + HeaderCard newCard = new HeaderCard(headerTag.toString(), headerTag.value(), headerTag.comment()); + fitsKV.put(headerTag.toString(), newCard); + } + } + + public FitsHdr build() { + return new FitsHdr(this); + } + + /** + * Check to make sure global corners and center lat,lon have valid values. If not then reset to + * defaults + * + * @param thisBuilder + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder checkGlobalLatLon(FitsHdrBuilder thisBuilder) throws HeaderCardException { + + // check if this keyword exists first. If not, then add all the corner keywords! + if (!thisBuilder.containsKey(HeaderTag.LLCLNG)) { + thisBuilder.addCard(HeaderTag.LLCLNG); + thisBuilder.addCard(HeaderTag.LLCLAT); + thisBuilder.addCard(HeaderTag.ULCLAT); + thisBuilder.addCard(HeaderTag.ULCLNG); + thisBuilder.addCard(HeaderTag.URCLAT); + thisBuilder.addCard(HeaderTag.URCLNG); + thisBuilder.addCard(HeaderTag.LRCLAT); + thisBuilder.addCard(HeaderTag.LRCLNG); + } + + if (!validLatLon(thisBuilder, HeaderTag.LLCLNG)) { + System.out.println("WARNING! check of global corners shows they have not been set!"); + System.out.println("Setting to default global values!"); + thisBuilder.setVbyHeaderTag(HeaderTag.LLCLNG, -180D); + thisBuilder.setVbyHeaderTag(HeaderTag.LLCLAT, -90D); + thisBuilder.setVbyHeaderTag(HeaderTag.ULCLNG, -180D); + thisBuilder.setVbyHeaderTag(HeaderTag.ULCLAT, 90D); + thisBuilder.setVbyHeaderTag(HeaderTag.URCLNG, 180D); + thisBuilder.setVbyHeaderTag(HeaderTag.URCLAT, 90D); + thisBuilder.setVbyHeaderTag(HeaderTag.LRCLNG, 180D); + thisBuilder.setVbyHeaderTag(HeaderTag.LRCLAT, -90D); + } + + // check if this keyword exists first. If not then add the center lat,lon keywords! + if (!thisBuilder.containsKey(HeaderTag.CLAT)) { + thisBuilder.addCard(HeaderTag.CLAT); + thisBuilder.addCard(HeaderTag.CLON); + } + if ((!validLatLon(thisBuilder, HeaderTag.CLAT)) || (!validLatLon(thisBuilder, HeaderTag.CLON))) { + System.out.println("WARNING! check of global center keywords shows they" + + "have not been set! Setting to default global values (0,0)"); + LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); + thisBuilder.setCenterLatLon(llr); + } + return thisBuilder; + } + + /** + * Check to make sure local corners and center lat,lon have valid values. If not then throw + * RuntimeException. + * + * @param thisBuilder + */ + public static void checkLocalLatLon(FitsHdrBuilder thisBuilder) { + + HeaderTag corner = HeaderTag.LLCLNG; + throwBadLatLon(thisBuilder, corner); + corner = HeaderTag.LLCLAT; + throwBadLatLon(thisBuilder, corner); + + corner = HeaderTag.ULCLNG; + throwBadLatLon(thisBuilder, corner); + corner = HeaderTag.ULCLAT; + throwBadLatLon(thisBuilder, corner); + + corner = HeaderTag.URCLNG; + throwBadLatLon(thisBuilder, corner); + corner = HeaderTag.URCLAT; + throwBadLatLon(thisBuilder, corner); + + corner = HeaderTag.LRCLNG; + throwBadLatLon(thisBuilder, corner); + corner = HeaderTag.LRCLAT; + throwBadLatLon(thisBuilder, corner); + + corner = HeaderTag.CLAT; + throwBadLatLon(thisBuilder, corner); + + corner = HeaderTag.CLON; + throwBadLatLon(thisBuilder, corner); + } + + private static boolean validLatLon(FitsHdrBuilder thisBuilder, HeaderTag cornerTag) { + + String chkVal = thisBuilder.getCard(cornerTag).getValue(); + if ((chkVal.contains("-999")) || (chkVal.contains("NaN")) || (chkVal.length() < 1)) { + return false; + } + return true; + } + + private static void throwBadLatLon(FitsHdrBuilder thisBuilder, HeaderTag cornerTag) { + + if (!validLatLon(thisBuilder, cornerTag)) { + String chkVal = thisBuilder.getCard(cornerTag).getValue(); + String errMesg = ("ERROR!" + cornerTag.toString() + " in FitsHdrBuilder not valid:" + chkVal); + throw new RuntimeException(errMesg); + } + } + } // end static class FitsHdrBuilder /** - * Set the HeaderCards for HeaderTags associated with the corners of the data. Set value = "N/A" - * if input data is null. Will use the same longitude range for corner points as in the data. - * I.e. if the data uses -180 to 180 then will use -180 to 180 range for longitude corner - * points. If data uses 0 - 360 then will use 0-360 for longitude corner points. - * - * @param data + * Copy the fits header from fits file and use it to populate and return FitsHdrBuilder. There are + * NO checks to see if the keywords match those in the Toolkit and the hdrBuilder is NOT + * initialized with enums from HeaderTag! + * + * @param fitsFile * @return * @throws HeaderCardException */ - public FitsHdrBuilder setCornerCards(double[][][] data) throws HeaderCardException { - if (data != null) { + public static FitsHdrBuilder copyFitsHeader(File fitsFile) throws HeaderCardException { - System.out.println("Data not null"); - int height = data[0].length; - int width = data[0][0].length; - - double llclat = data[0][0][0]; - double llclng = data[1][0][0]; - double urclat = data[0][height - 1][width - 1]; - double urclng = data[1][height - 1][width - 1]; - double lrclat = data[0][0][width - 1]; - double lrclng = data[1][0][width - 1]; - double ulclat = data[0][height - 1][0]; - double ulclng = data[1][height - 1][0]; - setVCbyHeaderTag(HeaderTag.LLCLNG, llclng, HeaderTag.LLCLNG.comment()); - setVCbyHeaderTag(HeaderTag.LLCLAT, llclat, HeaderTag.LLCLAT.comment()); - setVCbyHeaderTag(HeaderTag.URCLNG, urclng, HeaderTag.URCLNG.comment()); - setVCbyHeaderTag(HeaderTag.URCLAT, urclat, HeaderTag.URCLAT.comment()); - setVCbyHeaderTag(HeaderTag.LRCLNG, lrclng, HeaderTag.LRCLNG.comment()); - setVCbyHeaderTag(HeaderTag.LRCLAT, lrclat, HeaderTag.LRCLAT.comment()); - setVCbyHeaderTag(HeaderTag.ULCLNG, ulclng, HeaderTag.ULCLNG.comment()); - setVCbyHeaderTag(HeaderTag.ULCLAT, ulclat, HeaderTag.ULCLAT.comment()); - - } else { - - System.out.println("Data is null"); - - setVCbyHeaderTag(HeaderTag.LLCLNG, "N/A", HeaderTag.LLCLNG.comment()); - setVCbyHeaderTag(HeaderTag.LLCLAT, "N/A", HeaderTag.LLCLAT.comment()); - setVCbyHeaderTag(HeaderTag.URCLNG, "N/A", HeaderTag.URCLNG.comment()); - setVCbyHeaderTag(HeaderTag.URCLAT, "N/A", HeaderTag.URCLAT.comment()); - setVCbyHeaderTag(HeaderTag.LRCLNG, "N/A", HeaderTag.LRCLNG.comment()); - setVCbyHeaderTag(HeaderTag.LRCLAT, "N/A", HeaderTag.LRCLAT.comment()); - setVCbyHeaderTag(HeaderTag.ULCLNG, "N/A", HeaderTag.ULCLNG.comment()); - setVCbyHeaderTag(HeaderTag.ULCLAT, "N/A", HeaderTag.ULCLAT.comment()); - - } - - return this; + FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); + try { + Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); + boolean initHdr = false; + hdrBuilder = copyFitsHeader(map, initHdr); + return hdrBuilder; + } catch (FitsException | IOException e) { + e.printStackTrace(); + String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" + + fitsFile.toString() + " for fits header!"; + System.err.println(errmesg); + System.exit(1); + } + return hdrBuilder; } /** - * Set the HeaderCards for HeaderTags associated with the center latitude, longitude - * - * @param llr + * Copy the fits header from fits file and use it to populate and return FitsHdrBuilder. There are + * NO checks to see if the keywords match those in the Toolkit. The user has the option to + * initializehdrBuilder with enums from HeaderTag or not. + * + * @param fitsFile + * @param initHdr - If true the initialize FitsHdrBuilder w/ the enums in HeaderTag. if false, + * then FitsHdrBuilder initialized with an empty map. * @return * @throws HeaderCardException */ - public FitsHdrBuilder setCenterLatLon(LatitudinalVector llr) throws HeaderCardException { - if (llr != null) { - setVCbyHeaderTag(HeaderTag.CLON, Math.toDegrees(llr.getLongitude()), - HeaderTag.CLON.comment()); - setVCbyHeaderTag(HeaderTag.CLAT, Math.toDegrees(llr.getLatitude()), - HeaderTag.CLAT.comment()); - } - return this; + public static FitsHdrBuilder copyFitsHeader(File fitsFile, boolean initHdr) throws HeaderCardException { + + FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); + try { + Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); + hdrBuilder = copyFitsHeader(map, initHdr); + return hdrBuilder; + } catch (FitsException | IOException e) { + e.printStackTrace(); + String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" + + fitsFile.toString() + " for fits header!"; + System.err.println(errmesg); + System.exit(1); + } + return hdrBuilder; } /** - * Set the HeaderCards for HeaderTags associated with center latitude, longitude where center - * lat,lon is defined by the vector 'V'. If 'V' is null then uses default global lat,lon if - * 'isGlobal', otherwise returns with lat,lon headercards set to "N/A"; - * - * @param V - * @param isGlobal + * Loops through map of fits header cards and use it to populate and return FitsHdrBuilder. There + * are NO checks to see if the keywords match those in the toolkit! + * + * @param map + * @param initHdr - If true the initialize FitsHdrBuilder w/ the enums in HeaderTag. if false, + * then FitsHdrBuilder initialized with an empty map. + * @throws HeaderCardException + */ + public static FitsHdrBuilder copyFitsHeader(Map map, boolean initHdr) + throws HeaderCardException { + + // initialize hdrBuilder with the full set of keywords specified in HeaderTag + FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); + if (initHdr) { + hdrBuilder = initHdrBuilder(); + } + + // loop through each of the HeaderCards in the map and see if any will help + // build the header + for (Map.Entry entry : map.entrySet()) { + HeaderCard thisCard = entry.getValue(); + hdrBuilder.setbyHeaderCard(thisCard); + } + return hdrBuilder; + } + + /** + * Overloaded method. This is the original implementation. Set the initHdr flag to False. + * + * @param map * @return * @throws HeaderCardException */ - public FitsHdrBuilder setCenterLatLon(double[] V, boolean isGlobal) throws HeaderCardException { - if (V != null) { - LatitudinalVector llr = CoordConverters.convertToLatitudinal(new UnwritableVectorIJK(V)); - setCenterLatLon(llr); + public static FitsHdrBuilder copyFitsHeader(Map map) throws HeaderCardException { - } else { + // initialize hdrBuilder with the full set of keywords specified in HeaderTag + boolean initHdr = false; + FitsHdrBuilder hdrBuilder = copyFitsHeader(map, initHdr); - if (isGlobal) { - LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); - setCenterLatLon(llr); + return hdrBuilder; + } + + /** + * Loop through a map of fits header cards to set headercards on an existing builder. If input + * headerBuilder is null then will generate a new FitsHdrBuilder, update it, and return it. + * + * @param map + * @param hdrBuilder + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder addToFitsHeader(Map map, FitsHdrBuilder hdrBuilder) + throws HeaderCardException { + + if (hdrBuilder == null) { + hdrBuilder = new FitsHdrBuilder(); + } + + for (Map.Entry entry : map.entrySet()) { + HeaderCard thisCard = entry.getValue(); + hdrBuilder.setbyHeaderCard(thisCard); + } + return hdrBuilder; + } + + /** + * Copy the fits header from fits file and use it to update an existing builder. If builder is + * null then will generate a new FitsHdrBuilder, update it, and return it. + * + * @param fitsFile + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder addToFitsHeader(File fitsFile, FitsHdrBuilder hdrBuilder) throws HeaderCardException { + + if (hdrBuilder == null) { + hdrBuilder = new FitsHdrBuilder(); + } + + try { + Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); + hdrBuilder = addToFitsHeader(map, hdrBuilder); + return hdrBuilder; + } catch (FitsException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" + + fitsFile.toString() + " for fits header!"; + System.err.println(errmesg); + System.exit(1); + } + return hdrBuilder; + } + + /** + * Initialize builder with all the keywords specified in HeaderTag enum. + * + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder initHdrBuilder() throws HeaderCardException { + FitsHdrBuilder builder = new FitsHdrBuilder(); + for (HeaderTag tag : HeaderTag.fitsKeywords) { + builder.fitsKV.put(tag.toString(), new HeaderCard(tag.toString(), tag.value(), tag.comment())); + } + return builder; + } + + /** + * Configures an existing FitsHdrBuilder according to values read from a fits configuration file. + * Skips lines that start with '//'; assumes these are comments in the fits configuration file. + * Does NOT check to see if keywords in configFile match keywords in HeaderTag. + * + * @param configFile + * @param hdrBuilder + * @return + * @throws HeaderCardException + * @throws IOException + */ + public static FitsHdrBuilder configHdrBuilder(String configFile, FitsHdrBuilder hdrBuilder) + throws HeaderCardException, IOException { + File checkF = new File(configFile); + if (!checkF.exists()) { + String errMesg = "ERROR:FITS header configuration file:" + configFile + " does not exist!"; + throw new RuntimeException(errMesg); + } + + if (hdrBuilder == null) { + System.out.println( + "builder passed to FitsHeader.configHdrBuilder() is null. Generating" + " new FitsHeaderBuilder"); + hdrBuilder = new FitsHdrBuilder(); + } + List content = FileUtils.readLines(new File(configFile), Charset.defaultCharset()); + boolean separatorFound = false; + for (String line : content) { + + if (line.startsWith("//")) { + // treat as a comment line. Skip and go to next line + } else { + + String[] keyval = line.split("#"); + if (keyval.length > 1) { + + separatorFound = true; + // check if there is a match w/ HeaderTags. Returns 'NOMATCH' if match not found + HeaderTag thisTag = HeaderTag.tagFromString(keyval[0]); + + // pass to fits header builder and see if it matches on a fits keyword + if (keyval.length == 2) { + // assume user only wants to overwrite the value portion. Leave the comments + // alone. + System.out.println("setting " + thisTag.toString() + " to " + keyval[1]); + hdrBuilder.setVbyHeaderTag(thisTag, keyval[1]); + } else if (keyval.length == 3) { + if (keyval[2].contains("null")) { + // user explicitly wants to override any comment in this header with null + hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], null); + } else { + System.out.println( + "setting " + thisTag.toString() + " to " + keyval[1] + ", comment to " + keyval[2]); + hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], keyval[2]); + } + } else { + System.out.println( + "Warning: the following line in the config file line has more than 2 colons:"); + System.out.println(line); + System.out.println("Cannot parse. skipping this line"); + } + } + } + } + if (!separatorFound) { + System.out.println("WARNING! The fits config file:" + configFile + + " does not appear to be a valid config file! There is no # separator!"); + } + return hdrBuilder; + } + + /** + * Initializes and returns a FitsHdrBuilder based on values read from a fits configuration file. + * + * @param configFile + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder initHdrBuilder(String configFile) throws HeaderCardException { + FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); + // try to load config file if it exists and modify fits header builder with it. + try { + configHdrBuilder(configFile, hdrBuilder); + } catch (IOException e) { + // TODO Auto-generated catch block + String errMesg = "Error trying to read config file:" + configFile; + throw new RuntimeException(errMesg); + } + return hdrBuilder; + } + + /** + * Return a new FitsHdrBuilder with EMPTY map! Call initHdrBuilder() to get a FitsHdrBuilder with + * an initialized map. + * + * @return + * @throws HeaderCardException + */ + public static FitsHdrBuilder getBuilder() throws HeaderCardException { + return new FitsHdrBuilder(); + } + + /** + * Return the desired HeaderCard. Return default value specified by HeaderTag if not found in + * FitsHdr. + * + * @param tag + * @return + * @throws HeaderCardException + */ + public HeaderCard getHeaderCard(HeaderTag tag) throws HeaderCardException { + String keyword = tag.toString(); + if (fitsKV.containsKey(keyword)) { + return fitsKV.get(keyword); } else { - // unknown. Don't know why we wouldn't know the center lat, lon - setVCbyHeaderTag(HeaderTag.CLON, "N/A", HeaderTag.CLON.comment()); - setVCbyHeaderTag(HeaderTag.CLAT, "N/A", HeaderTag.CLAT.comment()); - } - } - return this; - } - - /** - * Hardcode center lat, lon to be 0, 0. Do this only if you know this is a global fits file. - * - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setGlobalCenter() throws HeaderCardException { - LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); - setCenterLatLon(llr); - return this; - } - - /** - * Set HeaderCards for HeaderTags associated with the vector to center of image. Throws - * RuntimeException if V is null. - * - * @param V - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setCenterVec(double[] V) throws HeaderCardException { - if (V != null) { - setVCbyHeaderTag(HeaderTag.CNTR_V_X, V[0], HeaderTag.CNTR_V_X.comment()); - setVCbyHeaderTag(HeaderTag.CNTR_V_Y, V[1], HeaderTag.CNTR_V_Y.comment()); - setVCbyHeaderTag(HeaderTag.CNTR_V_Z, V[2], HeaderTag.CNTR_V_Z.comment()); - } else { - - String errMesg = "ERROR! Center Vector is null! It could be that center" - + " vector information is already contained in FitsHdrBuilder!"; - throw new RuntimeException(errMesg); - - } - return this; - } - - /** - * Use the current time when this method is called to set the date when product was produced - * - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setDateprod() throws HeaderCardException { - - // DateFormat dateFormat = new - // SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssssZ"); - // date-time terminates in 'Z' - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - Date date = new Date(); - setVbyHeaderTag(HeaderTag.DATEPRD, dateFormat.format(date)); - - return this; - } - - /** - * Set HeaderCards associated with unit vector Ux defining the reference frame - * - * @param unitV - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setUx(double[] unitV) throws HeaderCardException { - - if (unitV != null) { - setVCbyHeaderTag(HeaderTag.UX_X, unitV[0], null); - setVCbyHeaderTag(HeaderTag.UX_Y, unitV[1], null); - setVCbyHeaderTag(HeaderTag.UX_Z, unitV[2], null); - - } else { - String errMesg = "ERROR! UX Vector is null! It could be that UX" - + " vector information is already contained in FitsHdrBuilder!"; - throw new RuntimeException(errMesg); - - } - - return this; - } - - /** - * Set HeaderCards associated with unit vector Uy defining the reference frame - * - * @param unitV - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setUy(double[] unitV) throws HeaderCardException { - - if (unitV != null) { - - setVCbyHeaderTag(HeaderTag.UY_X, unitV[0], null); - setVCbyHeaderTag(HeaderTag.UY_Y, unitV[1], null); - setVCbyHeaderTag(HeaderTag.UY_Z, unitV[2], null); - } else { - String errMesg = "ERROR! UY Vector is null! It could be that UY" - + " vector information is already contained in FitsHdrBuilder!"; - throw new RuntimeException(errMesg); - - } - - return this; - - } - - /** - * Set HeaderCards associated with unit vector Uz defining the reference frame - * - * @param unitV - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setUz(double[] unitV) throws HeaderCardException { - - if (unitV != null) { - setVCbyHeaderTag(HeaderTag.UZ_X, unitV[0], null); - setVCbyHeaderTag(HeaderTag.UZ_Y, unitV[1], null); - setVCbyHeaderTag(HeaderTag.UZ_Z, unitV[2], null); - - } else { - String errMesg = "ERROR! UZ Vector is null! It could be that UZ" - + " vector information is already contained in FitsHdrBuilder!"; - throw new RuntimeException(errMesg); - - } - - return this; - - } - - /** - * Set the GSD and GSDI values based on input ground sample distance. - * - * @param gsd - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setGSD(double gsd) throws HeaderCardException { - - // determine if GSD is close to an integer value - long gsdRound = Math.round(gsd); - double gsdRounded = gsdRound; - System.out.println("gsd calculated to be:" + String.valueOf(gsdRounded)); - - if (Math.abs(gsd - gsdRounded) < 1.0e-5) { - gsd = gsdRounded; - } - - setVCbyHeaderTag(HeaderTag.GSD, gsd, HeaderTag.GSD.comment()); - - // use default units for GSDI. Round to nearest integer - String gsdiUnits = HeaderTag.GSDI.comment(); - gsdiUnits = gsdiUnits.replace("[unk]", "[mm]"); - setVCbyHeaderTag(HeaderTag.GSDI, gsdRounded, gsdiUnits); - - return this; - } - - /** - * Change the string value for a HeaderCard in the map. If it doesn't already exist then create - * a new HeaderCard. - * - * @param hdrTag - * @param value - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setVbyHeaderTag(HeaderTag hdrTag, String value) - throws HeaderCardException { - if (fitsKV.containsKey(hdrTag.toString())) { - - fitsKV.get(hdrTag.toString()).setValue(value); - - } else { - - if (printWarnings) { - System.out.println("WARNING! header:" + hdrTag.toString() + " is new. Will add it to " - + "fits header builder."); - } - - HeaderCard newCard = new HeaderCard(hdrTag.toString(), value, hdrTag.comment()); - fitsKV.put(hdrTag.toString(), newCard); - } - return this; - } - - /** - * Change the value for a HeaderCard in the map - * - * @param hdrTag - * @param value - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setVbyHeaderTag(HeaderTag hdrTag, double value) - throws HeaderCardException { - - String hdrKey = hdrTag.toString(); - if (fitsKV.containsKey(hdrKey)) { - - fitsKV.get(hdrKey).setValue(value); - - } else { - if (printWarnings) { - System.out.println( - "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); - } - HeaderCard newCard = new HeaderCard(hdrKey, value, hdrTag.comment()); - fitsKV.put(hdrTag.toString(), newCard); - } - - return this; - } - - /** - * Change the string value and comment for a HeaderCard in the map. HeaderCard stores value as a - * string and adds quotes around it in the fits header to indicate that it is a 'string' value - * - * @param hdrTag - * @param value - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setVCbyHeaderTag(HeaderTag hdrTag, String value, String comment) - throws HeaderCardException { - - String hdrKey = hdrTag.toString(); - if (fitsKV.containsKey(hdrKey)) { - fitsKV.get(hdrKey).setValue(value); - fitsKV.get(hdrKey).setComment(comment); - - } else { - if (printWarnings) { - System.out.println( - "WARNING! header:" + hdrTag + " is new. Will add it to " + "fits header builder."); - } - HeaderCard newCard = new HeaderCard(hdrKey, value, comment); - fitsKV.put(hdrKey, newCard); - - } - return this; - - } - - /** - * Change the comment for a given HeaderCard in the map. Leaves the value unchanged - * - * @param hdrTag - * @param comment - * @return - */ - public FitsHdrBuilder setCbyHeaderTag(HeaderTag hdrTag, String comment) { - String hdrkey = hdrTag.toString(); - if (fitsKV.containsKey(hdrkey)) { - - fitsKV.get(hdrkey).setComment(comment); - - } else { - String errMesg = "ERROR! HeaderTag:" + hdrTag + " does not exist in FitsHdrBuilder!"; - throw new RuntimeException(errMesg); - } - return this; - - } - - /** - * Change the double value and comment for a HeaderCard in the map. HeaderCard stores value as a - * string but does NOT add quotes around it in the fits header. - * - * @param hdrTag - * @param value - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setVCbyHeaderTag(HeaderTag hdrTag, double value, String comment) - throws HeaderCardException { - - String hdrKey = hdrTag.toString(); - if (fitsKV.containsKey(hdrKey)) { - - fitsKV.get(hdrKey).setValue(value); - fitsKV.get(hdrKey).setComment(comment); - - } else { - if (printWarnings) { - System.out.println( - "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); - } - HeaderCard newCard = new HeaderCard(hdrKey, value, comment); - fitsKV.put(hdrKey, newCard); - } - return this; - - } - - /** - * Update existing HeaderCard with input headercard. If it doesn't exist then add the headercard - * to the map. - * - * @param headerCard - * @return - */ - public FitsHdrBuilder setbyHeaderCard(HeaderCard headerCard) { - String hdrKey = headerCard.getKey(); - - if (!fitsKV.containsKey(hdrKey)) { - if (printWarnings) { - System.out.println( - "WARNING! header:" + hdrKey + " is new. Will add it to " + "fits header builder."); - } - - } - - fitsKV.put(hdrKey, headerCard); - return this; - } - - /** - * Convenience method for updating all possible keywords using information found in FitsData. - * - * @param fitsData - * @return - * @throws HeaderCardException - */ - public FitsHdrBuilder setByFitsData(FitsData fitsData) throws HeaderCardException { - - HeaderCard hdrCard; - - // set map type, global or local - String hdrKey = HeaderTag.MAP_TYPE.toString(); - String mapType = "global"; - if (!fitsData.isGlobal()) { - mapType = "local"; - } - if (fitsKV.containsKey(hdrKey)) { - fitsKV.get(hdrKey).setValue(mapType); - } else { - - // fitsKV is no longer initially populated, so just add this. - hdrCard = new HeaderCard(hdrKey, mapType, HeaderTag.MAP_TYPE.comment()); - fitsKV.put(hdrKey, hdrCard); - } - - this.setCornerCards(fitsData.getData()); - - /* - * extract map information from fitsData if present: vector to center: V unit vectors - * describing reference frame: UX, UY, UZ, gsd - */ - if (fitsData.hasV()) { - this.setCenterVec(fitsData.getV()).setCenterLatLon(fitsData.getV(), fitsData.isGlobal()); - } else { - - // no center vector in fitsData. Check if this is a global product. - if (fitsData.isGlobal()) { - - // set center lat, lon to be 0deg, 0deg - this.setGlobalCenter(); - } - - // for the case where there is no center vector in fitsData - // and it is NOT a global product, assume the info is already captured - // in fitsKV. - } - - if (fitsData.hasUnitv()) { - // assume all unit vectors are present - this.setUx(fitsData.getUnit(UnitDir.UX)).setUy(fitsData.getUnit(UnitDir.UY)) - .setUz(fitsData.getUnit(UnitDir.UZ)); - - } - - if (fitsData.hasGsd()) { - this.setGSD(fitsData.getGSD()); - } - - return this; - } - - private Map getMap() { - return fitsKV; - } - - /** - * Check whether map contains the String keyword. - * - * @return - */ - public boolean containsKey(String keyWord) { - - return fitsKV.containsKey(keyWord); - } - - public boolean containsKey(HeaderTag keyWord) { - return fitsKV.containsKey(keyWord.toString()); - } - - /** - * Search map and return the HeaderCard associated with the String keyword. Returns null if key - * not contained in map. - * - * @param keyWord - * @return - */ - public HeaderCard getCard(String keyWord) { - - return fitsKV.get(keyWord); - } - - public HeaderCard getCard(HeaderTag keyWord) { - return fitsKV.get(keyWord.toString()); - } - - /** - * Add default values for the header tag to fitsKV map. The method checks to see if the - * headertag exists in the map. If it does exist then it will not modify the headercard! - * - * @param headerTag - * @throws HeaderCardException - */ - public void addCard(HeaderTag headerTag) throws HeaderCardException { - - if (this.containsKey(headerTag)) { - System.out.println("WARNING: keyword:" + headerTag.toString() - + " already exists in map. Will not" + " add a default headercard!"); - } else { - - HeaderCard newCard = - new HeaderCard(headerTag.toString(), headerTag.value(), headerTag.comment()); - fitsKV.put(headerTag.toString(), newCard); - } - - } - - public FitsHdr build() { - return new FitsHdr(this); - } - - /** - * Check to make sure global corners and center lat,lon have valid values. If not then reset to - * defaults - * - * @param thisBuilder - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder checkGlobalLatLon(FitsHdrBuilder thisBuilder) - throws HeaderCardException { - - // check if this keyword exists first. If not, then add all the corner keywords! - if (!thisBuilder.containsKey(HeaderTag.LLCLNG)) { - thisBuilder.addCard(HeaderTag.LLCLNG); - thisBuilder.addCard(HeaderTag.LLCLAT); - thisBuilder.addCard(HeaderTag.ULCLAT); - thisBuilder.addCard(HeaderTag.ULCLNG); - thisBuilder.addCard(HeaderTag.URCLAT); - thisBuilder.addCard(HeaderTag.URCLNG); - thisBuilder.addCard(HeaderTag.LRCLAT); - thisBuilder.addCard(HeaderTag.LRCLNG); - } - - if (!validLatLon(thisBuilder, HeaderTag.LLCLNG)) { - System.out.println("WARNING! check of global corners shows they have not been set!"); - System.out.println("Setting to default global values!"); - thisBuilder.setVbyHeaderTag(HeaderTag.LLCLNG, -180D); - thisBuilder.setVbyHeaderTag(HeaderTag.LLCLAT, -90D); - thisBuilder.setVbyHeaderTag(HeaderTag.ULCLNG, -180D); - thisBuilder.setVbyHeaderTag(HeaderTag.ULCLAT, 90D); - thisBuilder.setVbyHeaderTag(HeaderTag.URCLNG, 180D); - thisBuilder.setVbyHeaderTag(HeaderTag.URCLAT, 90D); - thisBuilder.setVbyHeaderTag(HeaderTag.LRCLNG, 180D); - thisBuilder.setVbyHeaderTag(HeaderTag.LRCLAT, -90D); - - } - - // check if this keyword exists first. If not then add the center lat,lon keywords! - if (!thisBuilder.containsKey(HeaderTag.CLAT)) { - thisBuilder.addCard(HeaderTag.CLAT); - thisBuilder.addCard(HeaderTag.CLON); - - } - if ((!validLatLon(thisBuilder, HeaderTag.CLAT)) - || (!validLatLon(thisBuilder, HeaderTag.CLON))) { - System.out.println("WARNING! check of global center keywords shows they" - + "have not been set! Setting to default global values (0,0)"); - LatitudinalVector llr = new LatitudinalVector(1., 0., 0.); - thisBuilder.setCenterLatLon(llr); - } - return thisBuilder; - } - - /** - * Check to make sure local corners and center lat,lon have valid values. If not then throw - * RuntimeException. - * - * @param thisBuilder - */ - public static void checkLocalLatLon(FitsHdrBuilder thisBuilder) { - - HeaderTag corner = HeaderTag.LLCLNG; - throwBadLatLon(thisBuilder, corner); - corner = HeaderTag.LLCLAT; - throwBadLatLon(thisBuilder, corner); - - corner = HeaderTag.ULCLNG; - throwBadLatLon(thisBuilder, corner); - corner = HeaderTag.ULCLAT; - throwBadLatLon(thisBuilder, corner); - - corner = HeaderTag.URCLNG; - throwBadLatLon(thisBuilder, corner); - corner = HeaderTag.URCLAT; - throwBadLatLon(thisBuilder, corner); - - corner = HeaderTag.LRCLNG; - throwBadLatLon(thisBuilder, corner); - corner = HeaderTag.LRCLAT; - throwBadLatLon(thisBuilder, corner); - - corner = HeaderTag.CLAT; - throwBadLatLon(thisBuilder, corner); - - corner = HeaderTag.CLON; - throwBadLatLon(thisBuilder, corner); - - } - - private static boolean validLatLon(FitsHdrBuilder thisBuilder, HeaderTag cornerTag) { - - String chkVal = thisBuilder.getCard(cornerTag).getValue(); - if ((chkVal.contains("-999")) || (chkVal.contains("NaN")) || (chkVal.length() < 1)) { - return false; - } - return true; - } - - private static void throwBadLatLon(FitsHdrBuilder thisBuilder, HeaderTag cornerTag) { - - if (!validLatLon(thisBuilder, cornerTag)) { - String chkVal = thisBuilder.getCard(cornerTag).getValue(); - String errMesg = - ("ERROR!" + cornerTag.toString() + " in FitsHdrBuilder not valid:" + chkVal); - throw new RuntimeException(errMesg); - } - - } - - } // end static class FitsHdrBuilder - - /** - * Copy the fits header from fits file and use it to populate and return FitsHdrBuilder. There are - * NO checks to see if the keywords match those in the Toolkit and the hdrBuilder is NOT - * initialized with enums from HeaderTag! - * - * @param fitsFile - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder copyFitsHeader(File fitsFile) throws HeaderCardException { - - FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); - try { - Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); - boolean initHdr = false; - hdrBuilder = copyFitsHeader(map, initHdr); - return hdrBuilder; - } catch (FitsException | IOException e) { - e.printStackTrace(); - String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" - + fitsFile.toString() + " for fits header!"; - System.err.println(errmesg); - System.exit(1); - - } - return hdrBuilder; - } - - /** - * Copy the fits header from fits file and use it to populate and return FitsHdrBuilder. There are - * NO checks to see if the keywords match those in the Toolkit. The user has the option to - * initializehdrBuilder with enums from HeaderTag or not. - * - * @param fitsFile - * @param initHdr - If true the initialize FitsHdrBuilder w/ the enums in HeaderTag. if false, - * then FitsHdrBuilder initialized with an empty map. - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder copyFitsHeader(File fitsFile, boolean initHdr) - throws HeaderCardException { - - FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); - try { - Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); - hdrBuilder = copyFitsHeader(map, initHdr); - return hdrBuilder; - } catch (FitsException | IOException e) { - e.printStackTrace(); - String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" - + fitsFile.toString() + " for fits header!"; - System.err.println(errmesg); - System.exit(1); - - } - return hdrBuilder; - } - - - /** - * Loops through map of fits header cards and use it to populate and return FitsHdrBuilder. There - * are NO checks to see if the keywords match those in the toolkit! - * - * @param map - * @param initHdr - If true the initialize FitsHdrBuilder w/ the enums in HeaderTag. if false, - * then FitsHdrBuilder initialized with an empty map. - * @throws HeaderCardException - */ - public static FitsHdrBuilder copyFitsHeader(Map map, boolean initHdr) - throws HeaderCardException { - - // initialize hdrBuilder with the full set of keywords specified in HeaderTag - FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); - if (initHdr) { - hdrBuilder = initHdrBuilder(); - } - - // loop through each of the HeaderCards in the map and see if any will help - // build the header - for (Map.Entry entry : map.entrySet()) { - HeaderCard thisCard = entry.getValue(); - hdrBuilder.setbyHeaderCard(thisCard); - } - return hdrBuilder; - } - - /** - * Overloaded method. This is the original implementation. Set the initHdr flag to False. - * - * @param map - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder copyFitsHeader(Map map) - throws HeaderCardException { - - // initialize hdrBuilder with the full set of keywords specified in HeaderTag - boolean initHdr = false; - FitsHdrBuilder hdrBuilder = copyFitsHeader(map, initHdr); - - return hdrBuilder; - } - - /** - * Loop through a map of fits header cards to set headercards on an existing builder. If input - * headerBuilder is null then will generate a new FitsHdrBuilder, update it, and return it. - * - * @param map - * @param hdrBuilder - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder addToFitsHeader(Map map, - FitsHdrBuilder hdrBuilder) throws HeaderCardException { - - if (hdrBuilder == null) { - hdrBuilder = new FitsHdrBuilder(); - } - - for (Map.Entry entry : map.entrySet()) { - HeaderCard thisCard = entry.getValue(); - hdrBuilder.setbyHeaderCard(thisCard); - } - return hdrBuilder; - - } - - /** - * Copy the fits header from fits file and use it to update an existing builder. If builder is - * null then will generate a new FitsHdrBuilder, update it, and return it. - * - * @param fitsFile - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder addToFitsHeader(File fitsFile, FitsHdrBuilder hdrBuilder) - throws HeaderCardException { - - if (hdrBuilder == null) { - hdrBuilder = new FitsHdrBuilder(); - } - - try { - Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); - hdrBuilder = addToFitsHeader(map, hdrBuilder); - return hdrBuilder; - } catch (FitsException | IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" - + fitsFile.toString() + " for fits header!"; - System.err.println(errmesg); - System.exit(1); - - } - return hdrBuilder; - } - - /** - * Initialize builder with all the keywords specified in HeaderTag enum. - * - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder initHdrBuilder() throws HeaderCardException { - FitsHdrBuilder builder = new FitsHdrBuilder(); - for (HeaderTag tag : HeaderTag.fitsKeywords) { - builder.fitsKV.put(tag.toString(), - new HeaderCard(tag.toString(), tag.value(), tag.comment())); - } - return builder; - } - - /** - * Configures an existing FitsHdrBuilder according to values read from a fits configuration file. - * Skips lines that start with '//'; assumes these are comments in the fits configuration file. - * Does NOT check to see if keywords in configFile match keywords in HeaderTag. - * - * @param configFile - * @param hdrBuilder - * @return - * @throws HeaderCardException - * @throws IOException - */ - public static FitsHdrBuilder configHdrBuilder(String configFile, FitsHdrBuilder hdrBuilder) - throws HeaderCardException, IOException { - File checkF = new File(configFile); - if (!checkF.exists()) { - String errMesg = "ERROR:FITS header configuration file:" + configFile + " does not exist!"; - throw new RuntimeException(errMesg); - } - - if (hdrBuilder == null) { - System.out.println("builder passed to FitsHeader.configHdrBuilder() is null. Generating" - + " new FitsHeaderBuilder"); - hdrBuilder = new FitsHdrBuilder(); - } - List content = FileUtils.readLines(new File(configFile), Charset.defaultCharset()); - boolean separatorFound = false; - for (String line : content) { - - if (line.startsWith("//")) { - // treat as a comment line. Skip and go to next line - } else { - - String[] keyval = line.split("#"); - if (keyval.length > 1) { - - separatorFound = true; - // check if there is a match w/ HeaderTags. Returns 'NOMATCH' if match not found - HeaderTag thisTag = HeaderTag.tagFromString(keyval[0]); - - // pass to fits header builder and see if it matches on a fits keyword - if (keyval.length == 2) { - // assume user only wants to overwrite the value portion. Leave the comments - // alone. - System.out.println("setting " + thisTag.toString() + " to " + keyval[1]); - hdrBuilder.setVbyHeaderTag(thisTag, keyval[1]); - } else if (keyval.length == 3) { - if (keyval[2].contains("null")) { - // user explicitly wants to override any comment in this header with null - hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], null); - } else { - System.out.println("setting " + thisTag.toString() + " to " + keyval[1] - + ", comment to " + keyval[2]); - hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], keyval[2]); - } - } else { System.out.println( - "Warning: the following line in the config file line has more than 2 colons:"); - System.out.println(line); - System.out.println("Cannot parse. skipping this line"); - } + "WARNING! keyword:" + keyword + " not set in fits header builder." + " using default value."); + return new HeaderCard(tag.toString(), tag.value(), tag.comment()); + } + } + /** + * Returns the desired headercard with the value stored as a double. This guarantees that when the + * fits header is created, the value for this fits keyword will be displayed without quotes around + * it. In the FITS format this denotes that the value is a numerical value. Return default value + * specified by HeaderTag if not found in FitsHdr. + * + * @param tag + * @return + * @throws HeaderCardException + */ + public HeaderCard getHeaderCardD(HeaderTag tag) throws HeaderCardException { + + String keyword = tag.toString(); + if (fitsKV.containsKey(keyword)) { + HeaderCard thisCard = fitsKV.get(keyword); + return new HeaderCard( + thisCard.getKey(), + Double.parseDouble(thisCard.getValue().replace('d', 'e').replace('D', 'e')), + thisCard.getComment()); + + } else { + + System.out.println("WARNING! keyword:" + keyword + " not set in FitsHdr," + " using default value."); + return new HeaderCard(keyword, tag.value(), tag.comment()); + } + } + + /** + * Return the desired HeaderCard - the double value is set to the desired string format. Return + * default value if card is not found. + * + * @param tag + * @return + */ + public HeaderCard getHeaderCard(HeaderTag tag, String fmtS) throws HeaderCardException { + String keyword = tag.toString(); + if (fitsKV.containsKey(keyword)) { + HeaderCard thisCard = fitsKV.get(keyword); + double value = Double.parseDouble(thisCard.getValue()); + if (Double.isFinite(value)) value = Double.parseDouble(String.format(fmtS, value)); + return new HeaderCard(thisCard.getKey(), value, thisCard.getComment()); + } else { + String mesg = "WARNING! keyword:" + tag.toString() + " not set in FitsHdr," + " using default value."; + System.out.println(mesg); + return new HeaderCard(keyword, tag.value(), tag.comment()); + } + } + + /** + * Generic function that searches a FITS HDU Header and returns HeaderCard matching the keyword + * Returns null if HeaderCard not found. + * + * @param header + * @param keyword + * @return + */ + public static HeaderCard getHeaderCardorNull(Header header, String keyword) { + + HeaderCard matchCard = null; + Cursor iterator = header.iterator(); + boolean opDone = false; + while ((iterator.hasNext()) && (!opDone)) { + + HeaderCard thisCard = iterator.next(); + if (thisCard.getKey().equals(keyword)) { + matchCard = thisCard; + opDone = true; + } } - } + return matchCard; } - if (!separatorFound) { - System.out.println("WARNING! The fits config file:" + configFile - + " does not appear to be a valid config file! There is no # separator!"); - } - return hdrBuilder; - } - - /** - * Initializes and returns a FitsHdrBuilder based on values read from a fits configuration file. - * - * @param configFile - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder initHdrBuilder(String configFile) throws HeaderCardException { - FitsHdrBuilder hdrBuilder = new FitsHdrBuilder(); - // try to load config file if it exists and modify fits header builder with it. - try { - configHdrBuilder(configFile, hdrBuilder); - } catch (IOException e) { - // TODO Auto-generated catch block - String errMesg = "Error trying to read config file:" + configFile; - throw new RuntimeException(errMesg); - } - return hdrBuilder; - } - - /** - * Return a new FitsHdrBuilder with EMPTY map! Call initHdrBuilder() to get a FitsHdrBuilder with - * an initialized map. - * - * @return - * @throws HeaderCardException - */ - public static FitsHdrBuilder getBuilder() throws HeaderCardException { - return new FitsHdrBuilder(); - } - - /** - * Return the desired HeaderCard. Return default value specified by HeaderTag if not found in - * FitsHdr. - * - * @param tag - * @return - * @throws HeaderCardException - */ - public HeaderCard getHeaderCard(HeaderTag tag) throws HeaderCardException { - String keyword = tag.toString(); - if (fitsKV.containsKey(keyword)) { - return fitsKV.get(keyword); - } else { - - System.out.println("WARNING! keyword:" + keyword + " not set in fits header builder." - + " using default value."); - return new HeaderCard(tag.toString(), tag.value(), tag.comment()); - } - } - - /** - * Returns the desired headercard with the value stored as a double. This guarantees that when the - * fits header is created, the value for this fits keyword will be displayed without quotes around - * it. In the FITS format this denotes that the value is a numerical value. Return default value - * specified by HeaderTag if not found in FitsHdr. - * - * @param tag - * @return - * @throws HeaderCardException - */ - public HeaderCard getHeaderCardD(HeaderTag tag) throws HeaderCardException { - - String keyword = tag.toString(); - if (fitsKV.containsKey(keyword)) { - HeaderCard thisCard = fitsKV.get(keyword); - return new HeaderCard(thisCard.getKey(), - Double.parseDouble(thisCard.getValue().replace('d', 'e').replace('D', 'e')), - thisCard.getComment()); - - } else { - - System.out.println( - "WARNING! keyword:" + keyword + " not set in FitsHdr," + " using default value."); - return new HeaderCard(keyword, tag.value(), tag.comment()); - - } - - } - - /** - * Return the desired HeaderCard - the double value is set to the desired string format. Return - * default value if card is not found. - * - * @param tag - * @return - */ - public HeaderCard getHeaderCard(HeaderTag tag, String fmtS) throws HeaderCardException { - String keyword = tag.toString(); - if (fitsKV.containsKey(keyword)) { - HeaderCard thisCard = fitsKV.get(keyword); - double value = Double.parseDouble(thisCard.getValue()); - if (Double.isFinite(value)) - value = Double.parseDouble(String.format(fmtS, value)); - return new HeaderCard(thisCard.getKey(), value, thisCard.getComment()); - } else { - String mesg = - "WARNING! keyword:" + tag.toString() + " not set in FitsHdr," + " using default value."; - System.out.println(mesg); - return new HeaderCard(keyword, tag.value(), tag.comment()); - - } - } - - /** - * Generic function that searches a FITS HDU Header and returns HeaderCard matching the keyword - * Returns null if HeaderCard not found. - * - * @param header - * @param keyword - * @return - */ - public static HeaderCard getHeaderCardorNull(Header header, String keyword) { - - HeaderCard matchCard = null; - Cursor iterator = header.iterator(); - boolean opDone = false; - while ((iterator.hasNext()) && (!opDone)) { - - HeaderCard thisCard = iterator.next(); - if (thisCard.getKey().equals(keyword)) { - matchCard = thisCard; - opDone = true; - } - } - - return matchCard; - - } - } diff --git a/src/main/java/terrasaur/fits/FitsHeader.java b/src/main/java/terrasaur/fits/FitsHeader.java index 3cc7a46..eb4e39e 100644 --- a/src/main/java/terrasaur/fits/FitsHeader.java +++ b/src/main/java/terrasaur/fits/FitsHeader.java @@ -28,10 +28,10 @@ import java.nio.charset.Charset; import java.util.EnumMap; import java.util.List; import java.util.Map; -import org.apache.commons.io.FileUtils; import nom.tam.fits.FitsException; import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; +import org.apache.commons.io.FileUtils; import terrasaur.utils.StringUtil; /** @@ -44,663 +44,629 @@ import terrasaur.utils.StringUtil; @Deprecated public class FitsHeader { - public final FitsValCom bitPix; - public final FitsValCom nAxis1; - public final FitsValCom nAxis2; - public final FitsValCom nAxis3; + public final FitsValCom bitPix; + public final FitsValCom nAxis1; + public final FitsValCom nAxis2; + public final FitsValCom nAxis3; - // fits keywords common to all ALTWG products - public final FitsValCom hdrVers; - public final FitsValCom mission; - public final FitsValCom hostName; - public final FitsValCom target; - public final FitsValCom origin; - public final FitsValCom spocid; - public final FitsValCom sdparea; - public final FitsValCom sdpdesc; - public final FitsValCom missionPhase; - public final FitsValCom dataSource; - public final FitsValCom dataSourceV; - public final FitsValCom dataSourceS; - public final FitsValCom dataSourceFile; - public final FitsValCom datasrcd; - public final FitsValCom productName; - public final FitsValCom dateprd; - public final FitsValCom productType; - public final FitsValCom productVer; - public final FitsValCom author; + // fits keywords common to all ALTWG products + public final FitsValCom hdrVers; + public final FitsValCom mission; + public final FitsValCom hostName; + public final FitsValCom target; + public final FitsValCom origin; + public final FitsValCom spocid; + public final FitsValCom sdparea; + public final FitsValCom sdpdesc; + public final FitsValCom missionPhase; + public final FitsValCom dataSource; + public final FitsValCom dataSourceV; + public final FitsValCom dataSourceS; + public final FitsValCom dataSourceFile; + public final FitsValCom datasrcd; + public final FitsValCom productName; + public final FitsValCom dateprd; + public final FitsValCom productType; + public final FitsValCom productVer; + public final FitsValCom author; - // for ancillary fits products - // map Name(description): tilt, gravity, slope, etc - // map Type: global or local - public final FitsValCom objFile; - public final FitsValCom mapName; - public final FitsValCom mapType; + // for ancillary fits products + // map Name(description): tilt, gravity, slope, etc + // map Type: global or local + public final FitsValCom objFile; + public final FitsValCom mapName; + public final FitsValCom mapType; - // center lon, lat. common to both local and global anci fits - public final FitsValCom clon; - public final FitsValCom clat; + // center lon, lat. common to both local and global anci fits + public final FitsValCom clon; + public final FitsValCom clat; - // global anci fits geometry - public final FitsValCom minlon; - public final FitsValCom maxlon; - public final FitsValCom minlat; - public final FitsValCom maxlat; + // global anci fits geometry + public final FitsValCom minlon; + public final FitsValCom maxlon; + public final FitsValCom minlat; + public final FitsValCom maxlat; - // local anci fits geometry - public final FitsValCom ulcLon; - public final FitsValCom ulcLat; - public final FitsValCom llcLon; - public final FitsValCom llcLat; - public final FitsValCom lrcLon; - public final FitsValCom lrcLat; - public final FitsValCom urcLon; - public final FitsValCom urcLat; - public final FitsValCom cntr_v_x; - public final FitsValCom cntr_v_y; - public final FitsValCom cntr_v_z; - public final FitsValCom ux_x; - public final FitsValCom ux_y; - public final FitsValCom ux_z; - public final FitsValCom uy_x; - public final FitsValCom uy_y; - public final FitsValCom uy_z; - public final FitsValCom uz_x; - public final FitsValCom uz_y; - public final FitsValCom uz_z; - public final FitsValCom gsd; - public final FitsValCom gsdi; + // local anci fits geometry + public final FitsValCom ulcLon; + public final FitsValCom ulcLat; + public final FitsValCom llcLon; + public final FitsValCom llcLat; + public final FitsValCom lrcLon; + public final FitsValCom lrcLat; + public final FitsValCom urcLon; + public final FitsValCom urcLat; + public final FitsValCom cntr_v_x; + public final FitsValCom cntr_v_y; + public final FitsValCom cntr_v_z; + public final FitsValCom ux_x; + public final FitsValCom ux_y; + public final FitsValCom ux_z; + public final FitsValCom uy_x; + public final FitsValCom uy_y; + public final FitsValCom uy_z; + public final FitsValCom uz_x; + public final FitsValCom uz_y; + public final FitsValCom uz_z; + public final FitsValCom gsd; + public final FitsValCom gsdi; - // common to both local and global anci fits - public final FitsValCom sigma; - public final FitsValCom sigDef; - public final FitsValCom dqual1; - public final FitsValCom dqual2; - public final FitsValCom dsigDef; - public final FitsValCom density; - public final FitsValCom rotRate; - public final FitsValCom refPot; - public final FitsValCom tiltRad; - public final FitsValCom tiltMaj; - public final FitsValCom tiltMin; - public final FitsValCom tiltPa; - public final FitsValCom mapVer; + // common to both local and global anci fits + public final FitsValCom sigma; + public final FitsValCom sigDef; + public final FitsValCom dqual1; + public final FitsValCom dqual2; + public final FitsValCom dsigDef; + public final FitsValCom density; + public final FitsValCom rotRate; + public final FitsValCom refPot; + public final FitsValCom tiltRad; + public final FitsValCom tiltMaj; + public final FitsValCom tiltMin; + public final FitsValCom tiltPa; + public final FitsValCom mapVer; - public final EnumMap tag2valcom; + public final EnumMap tag2valcom; - private FitsHeader(FitsHeaderBuilder b) { - this.bitPix = b.bitPix; - this.nAxis1 = b.nAxis1; - this.nAxis2 = b.nAxis2; - this.nAxis3 = b.nAxis3; - this.hdrVers = b.hdrVers; - this.mission = b.mission; - this.hostName = b.hostName; - this.target = b.target; - this.origin = b.origin; - this.spocid = b.spocid; - this.sdparea = b.sdparea; - this.sdpdesc = b.sdpdesc; - this.missionPhase = b.missionPhase; - this.dataSource = b.dataSource; - this.dataSourceFile = b.dataSourceFile; - this.dataSourceV = b.dataSourceV; - this.datasrcd = b.datasrcd; - this.dataSourceS = b.dataSourceS; - this.productName = b.productName; - this.dateprd = b.dateprd; - this.productType = b.productType; - this.productVer = b.productVer; - this.objFile = b.objFile; - this.author = b.author; - this.mapName = b.mapName; - this.mapType = b.mapType; - this.clon = b.clon; - this.clat = b.clat; - this.minlon = b.minlon; - this.maxlon = b.maxlon; - this.minlat = b.minlat; - this.maxlat = b.maxlat; - this.ulcLon = b.ulcLon; - this.ulcLat = b.ulcLat; - this.llcLon = b.llcLon; - this.llcLat = b.llcLat; - this.lrcLon = b.lrcLon; - this.lrcLat = b.lrcLat; - this.urcLon = b.urcLon; - this.urcLat = b.urcLat; - this.cntr_v_x = b.cntr_v_x; - this.cntr_v_y = b.cntr_v_y; - this.cntr_v_z = b.cntr_v_z; - this.ux_x = b.ux_x; - this.ux_y = b.ux_y; - this.ux_z = b.ux_z; - this.uy_x = b.uy_x; - this.uy_y = b.uy_y; - this.uy_z = b.uy_z; - this.uz_x = b.uz_x; - this.uz_y = b.uz_y; - this.uz_z = b.uz_z; - this.gsd = b.gsd; - this.gsdi = b.gsdi; - this.sigma = b.sigma; - this.sigDef = b.sigDef; - this.dqual1 = b.dqual1; - this.dqual2 = b.dqual2; - this.dsigDef = b.dsigDef; - this.mapVer = b.mapVer; - this.density = b.density; - this.rotRate = b.rotRate; - this.refPot = b.refPot; - this.tiltRad = b.tiltRad; - - // hardcode semi-major and semi-minor axis = radius - // since we only deal with circles for now - this.tiltMaj = b.tiltRad; - this.tiltMin = b.tiltRad; - this.tiltPa = b.tiltPa; - this.tag2valcom = b.tag2valcom; - } - - public static class FitsHeaderBuilder { - - // initialize the FITS keywords. Some of them may not change during the mission, but - // the option to change them is given via the public methods. - private FitsValCom bitPix = new FitsValCom("32", null); - private FitsValCom nAxis1 = new FitsValCom("1024", null); - private FitsValCom nAxis2 = new FitsValCom("1024", null); - private FitsValCom nAxis3 = new FitsValCom("numberPlanesNotSet", null); - - private FitsValCom hdrVers = - new FitsValCom(HeaderTag.HDRVERS.value(), HeaderTag.HDRVERS.comment()); - private FitsValCom mission = - new FitsValCom(HeaderTag.MISSION.value(), HeaderTag.MISSION.comment()); - private FitsValCom hostName = new FitsValCom(HeaderTag.HOSTNAME.value(), null); - private FitsValCom target = new FitsValCom(HeaderTag.TARGET.value(), null); - private FitsValCom origin = - new FitsValCom(HeaderTag.ORIGIN.value(), HeaderTag.ORIGIN.comment()); - private FitsValCom spocid = - new FitsValCom(HeaderTag.SPOC_ID.value(), HeaderTag.SPOC_ID.comment()); - private FitsValCom sdparea = - new FitsValCom(HeaderTag.SDPAREA.value(), HeaderTag.SDPAREA.comment()); - private FitsValCom sdpdesc = - new FitsValCom(HeaderTag.SDPDESC.value(), HeaderTag.SDPDESC.comment()); - private FitsValCom missionPhase = - new FitsValCom(HeaderTag.MPHASE.value(), HeaderTag.MPHASE.comment()); - private FitsValCom dataSource = - new FitsValCom(HeaderTag.DATASRC.value(), HeaderTag.DATASRC.comment()); - private FitsValCom dataSourceFile = - new FitsValCom(HeaderTag.DATASRCF.value(), HeaderTag.DATASRCF.comment()); - private FitsValCom dataSourceS = - new FitsValCom(HeaderTag.DATASRCS.value(), HeaderTag.DATASRCS.comment()); - private FitsValCom dataSourceV = - new FitsValCom(HeaderTag.DATASRCV.value(), HeaderTag.DATASRCV.comment()); - private FitsValCom software = - new FitsValCom(HeaderTag.SOFTWARE.value(), HeaderTag.SOFTWARE.comment()); - private FitsValCom softver = - new FitsValCom(HeaderTag.SOFT_VER.value(), HeaderTag.SOFT_VER.comment()); - private FitsValCom datasrcd = - new FitsValCom(HeaderTag.DATASRCD.value(), HeaderTag.DATASRCD.comment()); - private FitsValCom productName = - new FitsValCom(HeaderTag.PRODNAME.value(), HeaderTag.PRODNAME.comment()); - private FitsValCom dateprd = - new FitsValCom(HeaderTag.DATEPRD.value(), HeaderTag.DATEPRD.comment()); - private FitsValCom productType = new FitsValCom("productTypeNotSet", null); - private FitsValCom productVer = - new FitsValCom(HeaderTag.PRODVERS.value(), HeaderTag.PRODVERS.comment()); - private FitsValCom mapVer = - new FitsValCom(HeaderTag.MAP_VER.value(), HeaderTag.MAP_VER.comment()); - - private FitsValCom objFile = - new FitsValCom(HeaderTag.OBJ_FILE.value(), HeaderTag.OBJ_FILE.comment()); - private FitsValCom author = - new FitsValCom(HeaderTag.CREATOR.value(), HeaderTag.CREATOR.comment()); - - private FitsValCom mapName = - new FitsValCom(HeaderTag.MAP_NAME.value(), HeaderTag.MAP_NAME.comment()); - private FitsValCom mapType = - new FitsValCom(HeaderTag.MAP_TYPE.value(), HeaderTag.MAP_TYPE.comment()); - private FitsValCom clon = new FitsValCom("-999", HeaderTag.CLON.comment()); - private FitsValCom clat = new FitsValCom("-999", HeaderTag.CLAT.comment()); - private FitsValCom minlon = new FitsValCom("-999", HeaderTag.MINLON.comment()); - private FitsValCom maxlon = new FitsValCom("-999", HeaderTag.MAXLON.comment()); - private FitsValCom minlat = new FitsValCom("-999", HeaderTag.MINLAT.comment()); - private FitsValCom maxlat = new FitsValCom("-999", HeaderTag.MAXLAT.comment()); - private FitsValCom pxperdeg = new FitsValCom("-999", HeaderTag.PXPERDEG.comment()); - private FitsValCom ulcLon = new FitsValCom("-999", HeaderTag.ULCLNG.comment()); - private FitsValCom ulcLat = new FitsValCom("-999", HeaderTag.ULCLAT.comment()); - private FitsValCom llcLon = new FitsValCom("-999", HeaderTag.LLCLNG.comment()); - private FitsValCom llcLat = new FitsValCom("-999", HeaderTag.LLCLAT.comment()); - private FitsValCom lrcLon = new FitsValCom("-999", HeaderTag.LRCLNG.comment()); - private FitsValCom lrcLat = new FitsValCom("-999", HeaderTag.LRCLAT.comment()); - private FitsValCom urcLon = new FitsValCom("-999", HeaderTag.URCLNG.comment()); - private FitsValCom urcLat = new FitsValCom("-999", HeaderTag.URCLAT.comment()); - private FitsValCom cntr_v_x = new FitsValCom("-999", HeaderTag.CNTR_V_X.comment()); - private FitsValCom cntr_v_y = new FitsValCom("-999", HeaderTag.CNTR_V_Y.comment()); - private FitsValCom cntr_v_z = new FitsValCom("-999", HeaderTag.CNTR_V_Z.comment()); - private FitsValCom ux_x = new FitsValCom("-999", HeaderTag.UX_X.comment()); - private FitsValCom ux_y = new FitsValCom("-999", HeaderTag.UX_Y.comment()); - private FitsValCom ux_z = new FitsValCom("-999", HeaderTag.UX_Z.comment()); - private FitsValCom uy_x = new FitsValCom("-999", HeaderTag.UY_X.comment()); - private FitsValCom uy_y = new FitsValCom("-999", HeaderTag.UY_Y.comment()); - private FitsValCom uy_z = new FitsValCom("-999", HeaderTag.UY_Z.comment()); - private FitsValCom uz_x = new FitsValCom("-999", HeaderTag.UZ_X.comment()); - private FitsValCom uz_y = new FitsValCom("-999", HeaderTag.UZ_Y.comment()); - private FitsValCom uz_z = new FitsValCom("-999", HeaderTag.UZ_Z.comment()); - private FitsValCom gsd = new FitsValCom("-999", HeaderTag.GSD.comment()); - private FitsValCom gsdi = new FitsValCom("-999", HeaderTag.GSDI.comment()); - private FitsValCom sigma = new FitsValCom("-999", "N/A"); - private FitsValCom sigDef = new FitsValCom(HeaderTag.SIGMA.value(), HeaderTag.SIGMA.comment()); - private FitsValCom dqual1 = new FitsValCom("-999", HeaderTag.DQUAL_1.comment()); - private FitsValCom dqual2 = new FitsValCom("-999", HeaderTag.DQUAL_2.comment()); - private FitsValCom dsigDef = - new FitsValCom(HeaderTag.DSIG_DEF.value(), HeaderTag.DSIG_DEF.comment()); - private FitsValCom density = new FitsValCom("-999", HeaderTag.DENSITY.comment()); - private FitsValCom rotRate = new FitsValCom("-999", HeaderTag.ROT_RATE.comment()); - private FitsValCom refPot = new FitsValCom("-999", HeaderTag.REF_POT.comment()); - private FitsValCom tiltRad = new FitsValCom("-999", HeaderTag.TILT_RAD.comment()); - private FitsValCom tiltMaj = new FitsValCom("-999", HeaderTag.TILT_MAJ.comment()); - private FitsValCom tiltMin = new FitsValCom("-999", HeaderTag.TILT_MIN.comment()); - private FitsValCom tiltPa = new FitsValCom("0", HeaderTag.TILT_PA.comment()); - - private EnumMap tag2valcom = - new EnumMap(HeaderTag.class); - - public FitsHeaderBuilder() { - - /* - * initialize the map between header tags and the fits val com variables. This allows us to - * use enumeration to select which of the fitsvalcom variables we want to update, eliminating - * the need for specific 'set' statements for each variable. - */ - tag2valcom.put(HeaderTag.HDRVERS, hdrVers); - tag2valcom.put(HeaderTag.MISSION, mission); - tag2valcom.put(HeaderTag.HOSTNAME, hostName); - tag2valcom.put(HeaderTag.TARGET, target); - tag2valcom.put(HeaderTag.ORIGIN, origin); - tag2valcom.put(HeaderTag.SPOC_ID, spocid); - tag2valcom.put(HeaderTag.SDPAREA, sdparea); - tag2valcom.put(HeaderTag.SDPDESC, sdpdesc); - tag2valcom.put(HeaderTag.MPHASE, missionPhase); - tag2valcom.put(HeaderTag.DATASRC, dataSource); - tag2valcom.put(HeaderTag.DATASRCV, dataSourceV); - tag2valcom.put(HeaderTag.DATASRCF, dataSourceFile); - tag2valcom.put(HeaderTag.DATASRCS, dataSourceS); - // removed from ALTWG keywords per Map Format SIS draft v2 - tag2valcom.put(HeaderTag.DATASRCD, datasrcd); - tag2valcom.put(HeaderTag.SOFTWARE, software); - tag2valcom.put(HeaderTag.SOFT_VER, softver); - tag2valcom.put(HeaderTag.PRODNAME, productName); - tag2valcom.put(HeaderTag.DATEPRD, dateprd); - tag2valcom.put(HeaderTag.PRODVERS, productVer); - tag2valcom.put(HeaderTag.MAP_VER, mapVer); - tag2valcom.put(HeaderTag.CREATOR, author); - tag2valcom.put(HeaderTag.OBJ_FILE, objFile); - tag2valcom.put(HeaderTag.CLON, clon); - tag2valcom.put(HeaderTag.CLAT, clat); - tag2valcom.put(HeaderTag.MINLON, minlon); - tag2valcom.put(HeaderTag.MAXLON, maxlon); - tag2valcom.put(HeaderTag.MINLAT, minlat); - tag2valcom.put(HeaderTag.MAXLAT, maxlat); - tag2valcom.put(HeaderTag.PXPERDEG, pxperdeg); - tag2valcom.put(HeaderTag.LLCLNG, llcLon); - tag2valcom.put(HeaderTag.LLCLAT, llcLat); - tag2valcom.put(HeaderTag.LRCLNG, lrcLon); - tag2valcom.put(HeaderTag.LRCLAT, lrcLat); - tag2valcom.put(HeaderTag.URCLNG, urcLon); - tag2valcom.put(HeaderTag.URCLAT, urcLat); - tag2valcom.put(HeaderTag.ULCLNG, ulcLon); - tag2valcom.put(HeaderTag.ULCLAT, ulcLat); - tag2valcom.put(HeaderTag.CNTR_V_X, cntr_v_x); - tag2valcom.put(HeaderTag.CNTR_V_Y, cntr_v_y); - tag2valcom.put(HeaderTag.CNTR_V_Z, cntr_v_z); - tag2valcom.put(HeaderTag.UX_X, ux_x); - tag2valcom.put(HeaderTag.UX_Y, ux_y); - tag2valcom.put(HeaderTag.UX_Z, ux_z); - tag2valcom.put(HeaderTag.UY_X, uy_x); - tag2valcom.put(HeaderTag.UY_Y, uy_y); - tag2valcom.put(HeaderTag.UY_Z, uy_z); - tag2valcom.put(HeaderTag.UZ_X, ux_x); - tag2valcom.put(HeaderTag.UZ_Y, ux_y); - tag2valcom.put(HeaderTag.UZ_Z, ux_z); - tag2valcom.put(HeaderTag.GSD, gsd); - tag2valcom.put(HeaderTag.GSDI, gsdi); - tag2valcom.put(HeaderTag.SIGMA, sigma); - tag2valcom.put(HeaderTag.SIG_DEF, sigDef); - tag2valcom.put(HeaderTag.DQUAL_1, dqual1); - tag2valcom.put(HeaderTag.DQUAL_2, dqual2); - tag2valcom.put(HeaderTag.DSIG_DEF, dsigDef); - tag2valcom.put(HeaderTag.DENSITY, density); - tag2valcom.put(HeaderTag.ROT_RATE, rotRate); - tag2valcom.put(HeaderTag.REF_POT, refPot); - tag2valcom.put(HeaderTag.TILT_RAD, tiltRad); - tag2valcom.put(HeaderTag.TILT_MAJ, tiltMaj); - tag2valcom.put(HeaderTag.TILT_MIN, tiltMin); - tag2valcom.put(HeaderTag.TILT_PA, tiltPa); - tag2valcom.put(HeaderTag.MAP_NAME, mapName); - tag2valcom.put(HeaderTag.MAP_TYPE, mapType); - tag2valcom.put(HeaderTag.MAP_VER, mapVer); + private FitsHeader(FitsHeaderBuilder b) { + this.bitPix = b.bitPix; + this.nAxis1 = b.nAxis1; + this.nAxis2 = b.nAxis2; + this.nAxis3 = b.nAxis3; + this.hdrVers = b.hdrVers; + this.mission = b.mission; + this.hostName = b.hostName; + this.target = b.target; + this.origin = b.origin; + this.spocid = b.spocid; + this.sdparea = b.sdparea; + this.sdpdesc = b.sdpdesc; + this.missionPhase = b.missionPhase; + this.dataSource = b.dataSource; + this.dataSourceFile = b.dataSourceFile; + this.dataSourceV = b.dataSourceV; + this.datasrcd = b.datasrcd; + this.dataSourceS = b.dataSourceS; + this.productName = b.productName; + this.dateprd = b.dateprd; + this.productType = b.productType; + this.productVer = b.productVer; + this.objFile = b.objFile; + this.author = b.author; + this.mapName = b.mapName; + this.mapType = b.mapType; + this.clon = b.clon; + this.clat = b.clat; + this.minlon = b.minlon; + this.maxlon = b.maxlon; + this.minlat = b.minlat; + this.maxlat = b.maxlat; + this.ulcLon = b.ulcLon; + this.ulcLat = b.ulcLat; + this.llcLon = b.llcLon; + this.llcLat = b.llcLat; + this.lrcLon = b.lrcLon; + this.lrcLat = b.lrcLat; + this.urcLon = b.urcLon; + this.urcLat = b.urcLat; + this.cntr_v_x = b.cntr_v_x; + this.cntr_v_y = b.cntr_v_y; + this.cntr_v_z = b.cntr_v_z; + this.ux_x = b.ux_x; + this.ux_y = b.ux_y; + this.ux_z = b.ux_z; + this.uy_x = b.uy_x; + this.uy_y = b.uy_y; + this.uy_z = b.uy_z; + this.uz_x = b.uz_x; + this.uz_y = b.uz_y; + this.uz_z = b.uz_z; + this.gsd = b.gsd; + this.gsdi = b.gsdi; + this.sigma = b.sigma; + this.sigDef = b.sigDef; + this.dqual1 = b.dqual1; + this.dqual2 = b.dqual2; + this.dsigDef = b.dsigDef; + this.mapVer = b.mapVer; + this.density = b.density; + this.rotRate = b.rotRate; + this.refPot = b.refPot; + this.tiltRad = b.tiltRad; + // hardcode semi-major and semi-minor axis = radius + // since we only deal with circles for now + this.tiltMaj = b.tiltRad; + this.tiltMin = b.tiltRad; + this.tiltPa = b.tiltPa; + this.tag2valcom = b.tag2valcom; } - public FitsHeaderBuilder setTarget(String val, String comment) { - this.target.setV(val); - this.target.setC(comment); - return this; - } + public static class FitsHeaderBuilder { - public FitsHeaderBuilder setBitPix(String val, String comment) { - this.bitPix.setV(val); - this.bitPix.setC(comment); - return this; - } + // initialize the FITS keywords. Some of them may not change during the mission, but + // the option to change them is given via the public methods. + private FitsValCom bitPix = new FitsValCom("32", null); + private FitsValCom nAxis1 = new FitsValCom("1024", null); + private FitsValCom nAxis2 = new FitsValCom("1024", null); + private FitsValCom nAxis3 = new FitsValCom("numberPlanesNotSet", null); - public FitsHeaderBuilder setNAx1(String val, String comment) { - this.nAxis1.setV(val); - this.nAxis1.setC(comment); - return this; - } + private FitsValCom hdrVers = new FitsValCom(HeaderTag.HDRVERS.value(), HeaderTag.HDRVERS.comment()); + private FitsValCom mission = new FitsValCom(HeaderTag.MISSION.value(), HeaderTag.MISSION.comment()); + private FitsValCom hostName = new FitsValCom(HeaderTag.HOSTNAME.value(), null); + private FitsValCom target = new FitsValCom(HeaderTag.TARGET.value(), null); + private FitsValCom origin = new FitsValCom(HeaderTag.ORIGIN.value(), HeaderTag.ORIGIN.comment()); + private FitsValCom spocid = new FitsValCom(HeaderTag.SPOC_ID.value(), HeaderTag.SPOC_ID.comment()); + private FitsValCom sdparea = new FitsValCom(HeaderTag.SDPAREA.value(), HeaderTag.SDPAREA.comment()); + private FitsValCom sdpdesc = new FitsValCom(HeaderTag.SDPDESC.value(), HeaderTag.SDPDESC.comment()); + private FitsValCom missionPhase = new FitsValCom(HeaderTag.MPHASE.value(), HeaderTag.MPHASE.comment()); + private FitsValCom dataSource = new FitsValCom(HeaderTag.DATASRC.value(), HeaderTag.DATASRC.comment()); + private FitsValCom dataSourceFile = new FitsValCom(HeaderTag.DATASRCF.value(), HeaderTag.DATASRCF.comment()); + private FitsValCom dataSourceS = new FitsValCom(HeaderTag.DATASRCS.value(), HeaderTag.DATASRCS.comment()); + private FitsValCom dataSourceV = new FitsValCom(HeaderTag.DATASRCV.value(), HeaderTag.DATASRCV.comment()); + private FitsValCom software = new FitsValCom(HeaderTag.SOFTWARE.value(), HeaderTag.SOFTWARE.comment()); + private FitsValCom softver = new FitsValCom(HeaderTag.SOFT_VER.value(), HeaderTag.SOFT_VER.comment()); + private FitsValCom datasrcd = new FitsValCom(HeaderTag.DATASRCD.value(), HeaderTag.DATASRCD.comment()); + private FitsValCom productName = new FitsValCom(HeaderTag.PRODNAME.value(), HeaderTag.PRODNAME.comment()); + private FitsValCom dateprd = new FitsValCom(HeaderTag.DATEPRD.value(), HeaderTag.DATEPRD.comment()); + private FitsValCom productType = new FitsValCom("productTypeNotSet", null); + private FitsValCom productVer = new FitsValCom(HeaderTag.PRODVERS.value(), HeaderTag.PRODVERS.comment()); + private FitsValCom mapVer = new FitsValCom(HeaderTag.MAP_VER.value(), HeaderTag.MAP_VER.comment()); - public FitsHeaderBuilder setNAx2(String val, String comment) { - this.nAxis2.setV(val); - this.nAxis2.setC(comment); - return this; - } + private FitsValCom objFile = new FitsValCom(HeaderTag.OBJ_FILE.value(), HeaderTag.OBJ_FILE.comment()); + private FitsValCom author = new FitsValCom(HeaderTag.CREATOR.value(), HeaderTag.CREATOR.comment()); - public FitsHeaderBuilder setNAx3(String val, String comment) { - this.nAxis3.setV(val); - this.nAxis3.setC(comment); - return this; - } + private FitsValCom mapName = new FitsValCom(HeaderTag.MAP_NAME.value(), HeaderTag.MAP_NAME.comment()); + private FitsValCom mapType = new FitsValCom(HeaderTag.MAP_TYPE.value(), HeaderTag.MAP_TYPE.comment()); + private FitsValCom clon = new FitsValCom("-999", HeaderTag.CLON.comment()); + private FitsValCom clat = new FitsValCom("-999", HeaderTag.CLAT.comment()); + private FitsValCom minlon = new FitsValCom("-999", HeaderTag.MINLON.comment()); + private FitsValCom maxlon = new FitsValCom("-999", HeaderTag.MAXLON.comment()); + private FitsValCom minlat = new FitsValCom("-999", HeaderTag.MINLAT.comment()); + private FitsValCom maxlat = new FitsValCom("-999", HeaderTag.MAXLAT.comment()); + private FitsValCom pxperdeg = new FitsValCom("-999", HeaderTag.PXPERDEG.comment()); + private FitsValCom ulcLon = new FitsValCom("-999", HeaderTag.ULCLNG.comment()); + private FitsValCom ulcLat = new FitsValCom("-999", HeaderTag.ULCLAT.comment()); + private FitsValCom llcLon = new FitsValCom("-999", HeaderTag.LLCLNG.comment()); + private FitsValCom llcLat = new FitsValCom("-999", HeaderTag.LLCLAT.comment()); + private FitsValCom lrcLon = new FitsValCom("-999", HeaderTag.LRCLNG.comment()); + private FitsValCom lrcLat = new FitsValCom("-999", HeaderTag.LRCLAT.comment()); + private FitsValCom urcLon = new FitsValCom("-999", HeaderTag.URCLNG.comment()); + private FitsValCom urcLat = new FitsValCom("-999", HeaderTag.URCLAT.comment()); + private FitsValCom cntr_v_x = new FitsValCom("-999", HeaderTag.CNTR_V_X.comment()); + private FitsValCom cntr_v_y = new FitsValCom("-999", HeaderTag.CNTR_V_Y.comment()); + private FitsValCom cntr_v_z = new FitsValCom("-999", HeaderTag.CNTR_V_Z.comment()); + private FitsValCom ux_x = new FitsValCom("-999", HeaderTag.UX_X.comment()); + private FitsValCom ux_y = new FitsValCom("-999", HeaderTag.UX_Y.comment()); + private FitsValCom ux_z = new FitsValCom("-999", HeaderTag.UX_Z.comment()); + private FitsValCom uy_x = new FitsValCom("-999", HeaderTag.UY_X.comment()); + private FitsValCom uy_y = new FitsValCom("-999", HeaderTag.UY_Y.comment()); + private FitsValCom uy_z = new FitsValCom("-999", HeaderTag.UY_Z.comment()); + private FitsValCom uz_x = new FitsValCom("-999", HeaderTag.UZ_X.comment()); + private FitsValCom uz_y = new FitsValCom("-999", HeaderTag.UZ_Y.comment()); + private FitsValCom uz_z = new FitsValCom("-999", HeaderTag.UZ_Z.comment()); + private FitsValCom gsd = new FitsValCom("-999", HeaderTag.GSD.comment()); + private FitsValCom gsdi = new FitsValCom("-999", HeaderTag.GSDI.comment()); + private FitsValCom sigma = new FitsValCom("-999", "N/A"); + private FitsValCom sigDef = new FitsValCom(HeaderTag.SIGMA.value(), HeaderTag.SIGMA.comment()); + private FitsValCom dqual1 = new FitsValCom("-999", HeaderTag.DQUAL_1.comment()); + private FitsValCom dqual2 = new FitsValCom("-999", HeaderTag.DQUAL_2.comment()); + private FitsValCom dsigDef = new FitsValCom(HeaderTag.DSIG_DEF.value(), HeaderTag.DSIG_DEF.comment()); + private FitsValCom density = new FitsValCom("-999", HeaderTag.DENSITY.comment()); + private FitsValCom rotRate = new FitsValCom("-999", HeaderTag.ROT_RATE.comment()); + private FitsValCom refPot = new FitsValCom("-999", HeaderTag.REF_POT.comment()); + private FitsValCom tiltRad = new FitsValCom("-999", HeaderTag.TILT_RAD.comment()); + private FitsValCom tiltMaj = new FitsValCom("-999", HeaderTag.TILT_MAJ.comment()); + private FitsValCom tiltMin = new FitsValCom("-999", HeaderTag.TILT_MIN.comment()); + private FitsValCom tiltPa = new FitsValCom("0", HeaderTag.TILT_PA.comment()); - public FitsHeaderBuilder setVCbyHeaderTag(HeaderTag hdrTag, String value, String comment) { + private EnumMap tag2valcom = new EnumMap(HeaderTag.class); - if (tag2valcom.containsKey(hdrTag)) { - tag2valcom.get(hdrTag).setVC(value, comment); - } - return this; - } + public FitsHeaderBuilder() { - public FitsHeaderBuilder setVbyHeaderTag(HeaderTag hdrTag, String value) { - if (tag2valcom.containsKey(hdrTag)) { - tag2valcom.get(hdrTag).setV(value); - } - return this; - } + /* + * initialize the map between header tags and the fits val com variables. This allows us to + * use enumeration to select which of the fitsvalcom variables we want to update, eliminating + * the need for specific 'set' statements for each variable. + */ + tag2valcom.put(HeaderTag.HDRVERS, hdrVers); + tag2valcom.put(HeaderTag.MISSION, mission); + tag2valcom.put(HeaderTag.HOSTNAME, hostName); + tag2valcom.put(HeaderTag.TARGET, target); + tag2valcom.put(HeaderTag.ORIGIN, origin); + tag2valcom.put(HeaderTag.SPOC_ID, spocid); + tag2valcom.put(HeaderTag.SDPAREA, sdparea); + tag2valcom.put(HeaderTag.SDPDESC, sdpdesc); + tag2valcom.put(HeaderTag.MPHASE, missionPhase); + tag2valcom.put(HeaderTag.DATASRC, dataSource); + tag2valcom.put(HeaderTag.DATASRCV, dataSourceV); + tag2valcom.put(HeaderTag.DATASRCF, dataSourceFile); + tag2valcom.put(HeaderTag.DATASRCS, dataSourceS); + // removed from ALTWG keywords per Map Format SIS draft v2 + tag2valcom.put(HeaderTag.DATASRCD, datasrcd); + tag2valcom.put(HeaderTag.SOFTWARE, software); + tag2valcom.put(HeaderTag.SOFT_VER, softver); + tag2valcom.put(HeaderTag.PRODNAME, productName); + tag2valcom.put(HeaderTag.DATEPRD, dateprd); + tag2valcom.put(HeaderTag.PRODVERS, productVer); + tag2valcom.put(HeaderTag.MAP_VER, mapVer); + tag2valcom.put(HeaderTag.CREATOR, author); + tag2valcom.put(HeaderTag.OBJ_FILE, objFile); + tag2valcom.put(HeaderTag.CLON, clon); + tag2valcom.put(HeaderTag.CLAT, clat); + tag2valcom.put(HeaderTag.MINLON, minlon); + tag2valcom.put(HeaderTag.MAXLON, maxlon); + tag2valcom.put(HeaderTag.MINLAT, minlat); + tag2valcom.put(HeaderTag.MAXLAT, maxlat); + tag2valcom.put(HeaderTag.PXPERDEG, pxperdeg); + tag2valcom.put(HeaderTag.LLCLNG, llcLon); + tag2valcom.put(HeaderTag.LLCLAT, llcLat); + tag2valcom.put(HeaderTag.LRCLNG, lrcLon); + tag2valcom.put(HeaderTag.LRCLAT, lrcLat); + tag2valcom.put(HeaderTag.URCLNG, urcLon); + tag2valcom.put(HeaderTag.URCLAT, urcLat); + tag2valcom.put(HeaderTag.ULCLNG, ulcLon); + tag2valcom.put(HeaderTag.ULCLAT, ulcLat); + tag2valcom.put(HeaderTag.CNTR_V_X, cntr_v_x); + tag2valcom.put(HeaderTag.CNTR_V_Y, cntr_v_y); + tag2valcom.put(HeaderTag.CNTR_V_Z, cntr_v_z); + tag2valcom.put(HeaderTag.UX_X, ux_x); + tag2valcom.put(HeaderTag.UX_Y, ux_y); + tag2valcom.put(HeaderTag.UX_Z, ux_z); + tag2valcom.put(HeaderTag.UY_X, uy_x); + tag2valcom.put(HeaderTag.UY_Y, uy_y); + tag2valcom.put(HeaderTag.UY_Z, uy_z); + tag2valcom.put(HeaderTag.UZ_X, ux_x); + tag2valcom.put(HeaderTag.UZ_Y, ux_y); + tag2valcom.put(HeaderTag.UZ_Z, ux_z); + tag2valcom.put(HeaderTag.GSD, gsd); + tag2valcom.put(HeaderTag.GSDI, gsdi); + tag2valcom.put(HeaderTag.SIGMA, sigma); + tag2valcom.put(HeaderTag.SIG_DEF, sigDef); + tag2valcom.put(HeaderTag.DQUAL_1, dqual1); + tag2valcom.put(HeaderTag.DQUAL_2, dqual2); + tag2valcom.put(HeaderTag.DSIG_DEF, dsigDef); + tag2valcom.put(HeaderTag.DENSITY, density); + tag2valcom.put(HeaderTag.ROT_RATE, rotRate); + tag2valcom.put(HeaderTag.REF_POT, refPot); + tag2valcom.put(HeaderTag.TILT_RAD, tiltRad); + tag2valcom.put(HeaderTag.TILT_MAJ, tiltMaj); + tag2valcom.put(HeaderTag.TILT_MIN, tiltMin); + tag2valcom.put(HeaderTag.TILT_PA, tiltPa); + tag2valcom.put(HeaderTag.MAP_NAME, mapName); + tag2valcom.put(HeaderTag.MAP_TYPE, mapType); + tag2valcom.put(HeaderTag.MAP_VER, mapVer); + } - public FitsHeaderBuilder setCbyHeaderTag(HeaderTag hdrTag, String comment) { - if (tag2valcom.containsKey(hdrTag)) { - tag2valcom.get(hdrTag).setC(comment); - } - return this; + public FitsHeaderBuilder setTarget(String val, String comment) { + this.target.setV(val); + this.target.setC(comment); + return this; + } + + public FitsHeaderBuilder setBitPix(String val, String comment) { + this.bitPix.setV(val); + this.bitPix.setC(comment); + return this; + } + + public FitsHeaderBuilder setNAx1(String val, String comment) { + this.nAxis1.setV(val); + this.nAxis1.setC(comment); + return this; + } + + public FitsHeaderBuilder setNAx2(String val, String comment) { + this.nAxis2.setV(val); + this.nAxis2.setC(comment); + return this; + } + + public FitsHeaderBuilder setNAx3(String val, String comment) { + this.nAxis3.setV(val); + this.nAxis3.setC(comment); + return this; + } + + public FitsHeaderBuilder setVCbyHeaderTag(HeaderTag hdrTag, String value, String comment) { + + if (tag2valcom.containsKey(hdrTag)) { + tag2valcom.get(hdrTag).setVC(value, comment); + } + return this; + } + + public FitsHeaderBuilder setVbyHeaderTag(HeaderTag hdrTag, String value) { + if (tag2valcom.containsKey(hdrTag)) { + tag2valcom.get(hdrTag).setV(value); + } + return this; + } + + public FitsHeaderBuilder setCbyHeaderTag(HeaderTag hdrTag, String comment) { + if (tag2valcom.containsKey(hdrTag)) { + tag2valcom.get(hdrTag).setC(comment); + } + return this; + } + + /** + * Set values in the ancillary header builder class by parsing the headerCard. Parse appropriate + * values as determined by the value of the headercard key. + * + * @param headerCard + * @return + */ + public FitsHeaderBuilder setbyHeaderCard(HeaderCard headerCard) { + HeaderTag hdrTag = HeaderTag.END; + try { + hdrTag = HeaderTag.valueOf(headerCard.getKey()); + setVCbyHeaderTag(hdrTag, headerCard.getValue(), headerCard.getComment()); + } catch (IllegalArgumentException e) { + if ((headerCard.getKey().contains("COMMENT")) + || (headerCard.getKey().contains("PLANE"))) { + } else { + System.out.println(headerCard.getKey() + " not a HeaderTag"); + } + } catch (NullPointerException ne) { + System.out.println("null pointer exception for:" + headerCard.getKey()); + } + + return this; + } + + public FitsHeader build() { + + return new FitsHeader(this); + } } /** - * Set values in the ancillary header builder class by parsing the headerCard. Parse appropriate - * values as determined by the value of the headercard key. - * - * @param headerCard + * Loops through map of fits header cards and tries to parse values relevant to the ALTWG Fits + * file. + * + * @param map + */ + public static FitsHeaderBuilder copyFitsHeader(Map map) { + + FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); + + // loop through each of the HeaderCards in the map and see if any will help build the altwg + // header + for (Map.Entry entry : map.entrySet()) { + HeaderCard thisCard = entry.getValue(); + hdrBuilder.setbyHeaderCard(thisCard); + } + return hdrBuilder; + } + + /** + * Copy the fits header from fits file and use it to populate and return FitsHeaderBuilder. + * + * @param fitsFile * @return */ - public FitsHeaderBuilder setbyHeaderCard(HeaderCard headerCard) { - HeaderTag hdrTag = HeaderTag.END; - try { - hdrTag = HeaderTag.valueOf(headerCard.getKey()); - setVCbyHeaderTag(hdrTag, headerCard.getValue(), headerCard.getComment()); - } catch (IllegalArgumentException e) { - if ((headerCard.getKey().contains("COMMENT")) || (headerCard.getKey().contains("PLANE"))) { - } else { - System.out.println(headerCard.getKey() + " not a HeaderTag"); + public static FitsHeaderBuilder copyFitsHeader(File fitsFile) { + + FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); + try { + Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getCanonicalPath()); + hdrBuilder = copyFitsHeader(map); + return hdrBuilder; + } catch (FitsException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" + + fitsFile.toString() + " for fits header!"; + System.err.println(errmesg); + System.exit(1); } - } catch (NullPointerException ne) { - System.out.println("null pointer exception for:" + headerCard.getKey()); - } - - return this; - + return hdrBuilder; } - public FitsHeader build() { + /** + * Parse a fits configuration file and update a FitsHeaderBuilder. The builder is used to generate + * a fits header. + * + * @param configFile + * @param hdrBuilder - can either be an existing builder or null. If null then will create and + * return a new builder. + * @return + * @throws IOException + */ + public static FitsHeaderBuilder configHdrBuilder(String configFile, FitsHeaderBuilder hdrBuilder) + throws IOException { - return new FitsHeader(this); - } + File checkF = new File(configFile); + if (!checkF.exists()) { + // System.out.println("ERROR:FITS header configuration file:" + configFile + " does not + // exist!"); + String errMesg = "ERROR:FITS header configuration file:" + configFile + " does not exist!"; + throw new RuntimeException(errMesg); + } - } - - /** - * Loops through map of fits header cards and tries to parse values relevant to the ALTWG Fits - * file. - * - * @param map - */ - public static FitsHeaderBuilder copyFitsHeader(Map map) { - - FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); - - // loop through each of the HeaderCards in the map and see if any will help build the altwg - // header - for (Map.Entry entry : map.entrySet()) { - HeaderCard thisCard = entry.getValue(); - hdrBuilder.setbyHeaderCard(thisCard); - } - return hdrBuilder; - } - - /** - * Copy the fits header from fits file and use it to populate and return FitsHeaderBuilder. - * - * @param fitsFile - * @return - */ - public static FitsHeaderBuilder copyFitsHeader(File fitsFile) { - - FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); - try { - Map map = FitsUtil.getFitsHeaderAsMap(fitsFile.getCanonicalPath()); - hdrBuilder = copyFitsHeader(map); - return hdrBuilder; - } catch (FitsException | IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - String errmesg = "ERROR in FitsHeader.copyFitsHeader()! " + " Unable to parse fits file:" - + fitsFile.toString() + " for fits header!"; - System.err.println(errmesg); - System.exit(1); - - } - return hdrBuilder; - } - - /** - * Parse a fits configuration file and update a FitsHeaderBuilder. The builder is used to generate - * a fits header. - * - * @param configFile - * @param hdrBuilder - can either be an existing builder or null. If null then will create and - * return a new builder. - * @return - * @throws IOException - */ - public static FitsHeaderBuilder configHdrBuilder(String configFile, FitsHeaderBuilder hdrBuilder) - throws IOException { - - File checkF = new File(configFile); - if (!checkF.exists()) { - // System.out.println("ERROR:FITS header configuration file:" + configFile + " does not - // exist!"); - String errMesg = "ERROR:FITS header configuration file:" + configFile + " does not exist!"; - throw new RuntimeException(errMesg); - } - - if (hdrBuilder == null) { - System.out.println("builder passed to FitsHeader.configHdrBuilder() is null. Generating" - + " new FitsHeaderBuilder"); - hdrBuilder = new FitsHeaderBuilder(); - } - List content = FileUtils.readLines(new File(configFile), Charset.defaultCharset()); - boolean separatorFound = false; - for (String line : content) { - - String[] keyval = line.split("#"); - if (keyval.length > 1) { - - separatorFound = true; - // check if there is a match w/ HeaderTags - HeaderTag thisTag = HeaderTag.tagFromString(keyval[0]); - - if (thisTag != HeaderTag.NOMATCH) { - // pass to fits header builder and see if it matches on a fits keyword - - if (keyval.length == 2) { - // assume user only wants to overwrite the value portion. Leave the comments alone. - System.out.println("setting " + thisTag.toString() + " to " + keyval[1]); - hdrBuilder.setVbyHeaderTag(thisTag, keyval[1]); - } else if (keyval.length == 3) { - if (keyval[2].contains("null")) { - // user explicitly wants to override any comment in this header with null - hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], null); - } else { - System.out.println("setting " + thisTag.toString() + " to " + keyval[1] - + ", comment to " + keyval[2]); - hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], keyval[2]); - } - } else { + if (hdrBuilder == null) { System.out.println( - "Warning: the following line in the config file line has more than 2 colons:"); - System.out.println(line); - System.out.println("Cannot parse. skipping this line"); - } - + "builder passed to FitsHeader.configHdrBuilder() is null. Generating" + " new FitsHeaderBuilder"); + hdrBuilder = new FitsHeaderBuilder(); } - } - } - if (!separatorFound) { - System.out.println("WARNING! The fits config file:" + configFile - + " does not appear to be a valid config file! There is no # separator!"); - } - return hdrBuilder; - } + List content = FileUtils.readLines(new File(configFile), Charset.defaultCharset()); + boolean separatorFound = false; + for (String line : content) { - /** - * Given a fits keyword return the object containing the keyword value and comment. - * - * @param tag - fits keyword as enumeration. - * @return HeaderCard - * @throws HeaderCardException - */ - public HeaderCard getHeaderCard(HeaderTag tag) throws HeaderCardException { + String[] keyval = line.split("#"); + if (keyval.length > 1) { - // format for double values - String fmtS = "%18.13f"; - if (tag2valcom.containsKey(tag)) { - FitsValCom valcom = tag2valcom.get(tag); + separatorFound = true; + // check if there is a match w/ HeaderTags + HeaderTag thisTag = HeaderTag.tagFromString(keyval[0]); - /* - * FitsValCom stores the values as String because extracting the value from the source fits - * file returns it as a string. Need to convert geometry values to double and store it in the - * HeaderCard as a double. Then when written to the fits file it will not have quotes around - * the value. - */ - - /* - * type of value to store in headercard. =0 string (single quotes around value) =1 formatted - * double =2 free-form double - */ - int returnType = 0; - - switch (tag) { - - case PXPERDEG: - case CLON: - case CLAT: - case MINLON: - case MAXLON: - case MINLAT: - case MAXLAT: - case URCLNG: - case URCLAT: - case LRCLNG: - case LRCLAT: - case LLCLNG: - case LLCLAT: - case ULCLNG: - case ULCLAT: - case GSDI: - returnType = 1; - break; - - case CNTR_V_X: - case CNTR_V_Y: - case CNTR_V_Z: - case UX_X: - case UX_Y: - case UX_Z: - case UY_X: - case UY_Y: - case UY_Z: - case UZ_X: - case UZ_Y: - case UZ_Z: - case DENSITY: - case ROT_RATE: - case REF_POT: - case TILT_RAD: - case TILT_MAJ: - case TILT_MIN: - case TILT_PA: - case GSD: - case SIGMA: - case DQUAL_1: - case DQUAL_2: - returnType = 2; - break; - - default: - returnType = 0; - break; - } - - switch (returnType) { - case 0: - return new HeaderCard(tag.toString(), valcom.getV(), valcom.getC()); - - case 1: - return new HeaderCard(tag.toString(), StringUtil.str2fmtD(fmtS, valcom.getV()), - valcom.getC()); - - case 2: - return new HeaderCard(tag.toString(), StringUtil.parseSafeD(valcom.getV()), - valcom.getC()); - - } + if (thisTag != HeaderTag.NOMATCH) { + // pass to fits header builder and see if it matches on a fits keyword + if (keyval.length == 2) { + // assume user only wants to overwrite the value portion. Leave the comments alone. + System.out.println("setting " + thisTag.toString() + " to " + keyval[1]); + hdrBuilder.setVbyHeaderTag(thisTag, keyval[1]); + } else if (keyval.length == 3) { + if (keyval[2].contains("null")) { + // user explicitly wants to override any comment in this header with null + hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], null); + } else { + System.out.println( + "setting " + thisTag.toString() + " to " + keyval[1] + ", comment to " + keyval[2]); + hdrBuilder.setVCbyHeaderTag(thisTag, keyval[1], keyval[2]); + } + } else { + System.out.println( + "Warning: the following line in the config file line has more than 2 colons:"); + System.out.println(line); + System.out.println("Cannot parse. skipping this line"); + } + } + } + } + if (!separatorFound) { + System.out.println("WARNING! The fits config file:" + configFile + + " does not appear to be a valid config file! There is no # separator!"); + } + return hdrBuilder; } - String errMesg = "ERROR!, cannot find fits keyword:" + tag.toString(); - throw new RuntimeException(errMesg); - } + /** + * Given a fits keyword return the object containing the keyword value and comment. + * + * @param tag - fits keyword as enumeration. + * @return HeaderCard + * @throws HeaderCardException + */ + public HeaderCard getHeaderCard(HeaderTag tag) throws HeaderCardException { - /** - * Initialize fits header builder for using keyword/values from a fits config file. Can be used to - * initialize headerBuilder for any fits file. - * - * @param configFile - * @return - */ - public static FitsHeaderBuilder initHdrBuilder(String configFile) { - FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); + // format for double values + String fmtS = "%18.13f"; + if (tag2valcom.containsKey(tag)) { + FitsValCom valcom = tag2valcom.get(tag); - if (configFile != null) { - // try to load config file if it exists and modify fits header builder with it. - try { - configHdrBuilder(configFile, hdrBuilder); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - System.out.println("Error trying to read config file:" + configFile); - } + /* + * FitsValCom stores the values as String because extracting the value from the source fits + * file returns it as a string. Need to convert geometry values to double and store it in the + * HeaderCard as a double. Then when written to the fits file it will not have quotes around + * the value. + */ + + /* + * type of value to store in headercard. =0 string (single quotes around value) =1 formatted + * double =2 free-form double + */ + int returnType = 0; + + switch (tag) { + case PXPERDEG: + case CLON: + case CLAT: + case MINLON: + case MAXLON: + case MINLAT: + case MAXLAT: + case URCLNG: + case URCLAT: + case LRCLNG: + case LRCLAT: + case LLCLNG: + case LLCLAT: + case ULCLNG: + case ULCLAT: + case GSDI: + returnType = 1; + break; + + case CNTR_V_X: + case CNTR_V_Y: + case CNTR_V_Z: + case UX_X: + case UX_Y: + case UX_Z: + case UY_X: + case UY_Y: + case UY_Z: + case UZ_X: + case UZ_Y: + case UZ_Z: + case DENSITY: + case ROT_RATE: + case REF_POT: + case TILT_RAD: + case TILT_MAJ: + case TILT_MIN: + case TILT_PA: + case GSD: + case SIGMA: + case DQUAL_1: + case DQUAL_2: + returnType = 2; + break; + + default: + returnType = 0; + break; + } + + switch (returnType) { + case 0: + return new HeaderCard(tag.toString(), valcom.getV(), valcom.getC()); + + case 1: + return new HeaderCard(tag.toString(), StringUtil.str2fmtD(fmtS, valcom.getV()), valcom.getC()); + + case 2: + return new HeaderCard(tag.toString(), StringUtil.parseSafeD(valcom.getV()), valcom.getC()); + } + } + + String errMesg = "ERROR!, cannot find fits keyword:" + tag.toString(); + throw new RuntimeException(errMesg); } - return hdrBuilder; - } + /** + * Initialize fits header builder for using keyword/values from a fits config file. Can be used to + * initialize headerBuilder for any fits file. + * + * @param configFile + * @return + */ + public static FitsHeaderBuilder initHdrBuilder(String configFile) { + FitsHeaderBuilder hdrBuilder = new FitsHeaderBuilder(); + + if (configFile != null) { + // try to load config file if it exists and modify fits header builder with it. + try { + configHdrBuilder(configFile, hdrBuilder); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + System.out.println("Error trying to read config file:" + configFile); + } + } + return hdrBuilder; + } } diff --git a/src/main/java/terrasaur/fits/FitsHeaderFactory.java b/src/main/java/terrasaur/fits/FitsHeaderFactory.java index 8728bf0..87d233b 100644 --- a/src/main/java/terrasaur/fits/FitsHeaderFactory.java +++ b/src/main/java/terrasaur/fits/FitsHeaderFactory.java @@ -33,203 +33,193 @@ import terrasaur.utils.DTMHeader; * Factory class that returns List where the HeaderCard objects are in the correct order * for writing to a fits header. Also contains methods for creating List for different * sections of a fits header - * + * * @author espirrc1 * */ public class FitsHeaderFactory { - private static final String PLANE = "PLANE"; - private static final String COMMENT = "COMMENT"; + private static final String PLANE = "PLANE"; + private static final String COMMENT = "COMMENT"; + public static DTMHeader getDTMHeader(FitsHdr fitsHdr, FitsHeaderType headerType) { - public static DTMHeader getDTMHeader(FitsHdr fitsHdr, FitsHeaderType headerType) { + switch (headerType) { + case NFTMLN: + return new NFTmln(fitsHdr); - switch (headerType) { + case DTMLOCALALTWG: + return new AltwgLocalDTM(fitsHdr); - case NFTMLN: - return new NFTmln(fitsHdr); + case DTMGLOBALALTWG: + return new AltwgGlobalDTM(fitsHdr); - case DTMLOCALALTWG: - return new AltwgLocalDTM(fitsHdr); + case DTMGLOBALGENERIC: + return new GenericGlobalDTM(fitsHdr); - case DTMGLOBALALTWG: - return new AltwgGlobalDTM(fitsHdr); + case DTMLOCALGENERIC: + return new GenericLocalDTM(fitsHdr); - case DTMGLOBALGENERIC: - return new GenericGlobalDTM(fitsHdr); - - case DTMLOCALGENERIC: - return new GenericLocalDTM(fitsHdr); - - default: - return null; + default: + return null; + } } - } + public static AnciFitsHeader getAnciHeader(FitsHdr fitsHdr, FitsHeaderType headerType) { - public static AnciFitsHeader getAnciHeader(FitsHdr fitsHdr, FitsHeaderType headerType) { + switch (headerType) { + case ANCIGLOBALGENERIC: + return new GenericAnciGlobal(fitsHdr); - switch (headerType) { - case ANCIGLOBALGENERIC: - return new GenericAnciGlobal(fitsHdr); + case ANCILOCALGENERIC: + return new GenericAnciLocal(fitsHdr); - case ANCILOCALGENERIC: - return new GenericAnciLocal(fitsHdr); + case ANCIGLOBALALTWG: + return new AltwgAnciGlobal(fitsHdr); - case ANCIGLOBALALTWG: - return new AltwgAnciGlobal(fitsHdr); + case ANCIG_FACETRELATION_ALTWG: + return new AltwgAnciGlobalFacetRelation(fitsHdr); - case ANCIG_FACETRELATION_ALTWG: - return new AltwgAnciGlobalFacetRelation(fitsHdr); + case ANCILOCALALTWG: + return new AltwgAnciLocal(fitsHdr); - case ANCILOCALALTWG: - return new AltwgAnciLocal(fitsHdr); - - default: - return null; + default: + return null; + } } - } + /** + * Fits Header block that contains information about the fits header itself. Ex. Header version + * number. + * + * @param fitsHdr + * @return + * @throws HeaderCardException + */ + public static List getHeaderInfo(FitsHeader fitsHdr) throws HeaderCardException { + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "header information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - /** - * Fits Header block that contains information about the fits header itself. Ex. Header version - * number. - * - * @param fitsHdr - * @return - * @throws HeaderCardException - */ - public static List getHeaderInfo(FitsHeader fitsHdr) throws HeaderCardException { - - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "header information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.HDRVERS)); - - return headers; - - } - - /** - * Fits header block that contains information about the mission Ex. MISSION name, HOST name, - * Target name. - * - * @param fitsHdr - * @return - * @throws HeaderCardException - */ - public static List getMissionInfo(FitsHeader fitsHdr) throws HeaderCardException { - - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "mission information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); - - return headers; - - } - - /** - * Fits header block that contains ID information, i.e. information that would uniquely identify - * the data product. - * - * @param fitsHdr - * @return - * @throws HeaderCardException - */ - public static List getIdInfo(FitsHeader fitsHdr) throws HeaderCardException { - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "identification info", false)); - - // check latest Map Format SIS revision to see if SPOC handles these keywords - headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); - - return headers; - - } - - /** - * Fits header block that contains information about the source shape model used to create the - * fits file. - * - * @param fitsHdr - * @return - * @throws HeaderCardException - */ - public static List getShapeSourceInfo(FitsHeader fitsHdr) throws HeaderCardException { - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "shape data source", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); - - return headers; - } - - public static List getProcInfo(FitsHeader fitsHdr) throws HeaderCardException { - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "processing information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - - return headers; - - } - - public static List getMapSpecificInfo(FitsHeader fitsHdr) throws HeaderCardException { - - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "map specific information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); - - return headers; - - // check latest Map Format SIS revision to see if SPOC handles these keywords - // MAP_PROJ* - // GSD* - // GSDI* - } - - public static List getSummarySpatialInfo(FitsHeader fitsHdr, - FitsHeaderType fitsHeaderType) throws HeaderCardException { - - List headers = new ArrayList(); - - // this section common to all fitsHeaderTypes - headers.add(new HeaderCard(COMMENT, "summary spatial information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.CLAT)); - - switch (fitsHeaderType) { - case ANCILOCALGENERIC: - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT)); - break; - - default: - // default does nothing because switch only handles specific cases. - break; + return headers; } - return headers; - } + /** + * Fits header block that contains information about the mission Ex. MISSION name, HOST name, + * Target name. + * + * @param fitsHdr + * @return + * @throws HeaderCardException + */ + public static List getMissionInfo(FitsHeader fitsHdr) throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "mission information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MISSION)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.HOSTNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.TARGET)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ORIGIN)); + + return headers; + } + + /** + * Fits header block that contains ID information, i.e. information that would uniquely identify + * the data product. + * + * @param fitsHdr + * @return + * @throws HeaderCardException + */ + public static List getIdInfo(FitsHeader fitsHdr) throws HeaderCardException { + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "identification info", false)); + + // check latest Map Format SIS revision to see if SPOC handles these keywords + headers.add(fitsHdr.getHeaderCard(HeaderTag.SPOC_ID)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPAREA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SDPDESC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MPHASE)); + + return headers; + } + + /** + * Fits header block that contains information about the source shape model used to create the + * fits file. + * + * @param fitsHdr + * @return + * @throws HeaderCardException + */ + public static List getShapeSourceInfo(FitsHeader fitsHdr) throws HeaderCardException { + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "shape data source", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.OBJ_FILE)); + + return headers; + } + + public static List getProcInfo(FitsHeader fitsHdr) throws HeaderCardException { + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "processing information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATEPRD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); + + return headers; + } + + public static List getMapSpecificInfo(FitsHeader fitsHdr) throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "map specific information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_NAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_VER)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.MAP_TYPE)); + + return headers; + + // check latest Map Format SIS revision to see if SPOC handles these keywords + // MAP_PROJ* + // GSD* + // GSDI* + } + + public static List getSummarySpatialInfo(FitsHeader fitsHdr, FitsHeaderType fitsHeaderType) + throws HeaderCardException { + + List headers = new ArrayList(); + + // this section common to all fitsHeaderTypes + headers.add(new HeaderCard(COMMENT, "summary spatial information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.CLAT)); + + switch (fitsHeaderType) { + case ANCILOCALGENERIC: + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLNG)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LLCLAT)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLNG)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.URCLAT)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLNG)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.LRCLAT)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLNG)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.ULCLAT)); + break; + + default: + // default does nothing because switch only handles specific cases. + break; + } + return headers; + } } diff --git a/src/main/java/terrasaur/fits/FitsUtil.java b/src/main/java/terrasaur/fits/FitsUtil.java index 5506c94..b61e0ef 100644 --- a/src/main/java/terrasaur/fits/FitsUtil.java +++ b/src/main/java/terrasaur/fits/FitsUtil.java @@ -28,7 +28,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import nom.tam.fits.BasicHDU; import nom.tam.fits.Fits; import nom.tam.fits.FitsException; @@ -37,568 +36,555 @@ import nom.tam.fits.HeaderCard; import nom.tam.fits.TableHDU; import nom.tam.util.BufferedFile; import nom.tam.util.Cursor; +import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; /** * Utility class containing generic static routines for working with FITS files. Refer to AltwgFits * for static routines pertaining to ALTWG fit image files. - * + * * @author kahneg1 * @version 1.0 - * + * */ public class FitsUtil { - // private static final String PLANE = "PLANE"; - // public static final String COMMENT = "COMMENT"; + // private static final String PLANE = "PLANE"; + // public static final String COMMENT = "COMMENT"; - public static double[][][] loadFits(String filename, int[] axes) - throws FitsException, IOException { - Fits f = new Fits(filename); - BasicHDU hdu = f.getHDU(0); - int[] axes2 = hdu.getAxes(); - if (axes2.length != 3) { - throw new IOException("FITS file has incorrect dimensions"); + public static double[][][] loadFits(String filename, int[] axes) throws FitsException, IOException { + Fits f = new Fits(filename); + BasicHDU hdu = f.getHDU(0); + int[] axes2 = hdu.getAxes(); + if (axes2.length != 3) { + throw new IOException("FITS file has incorrect dimensions"); + } + + axes[0] = axes2[0]; + axes[1] = axes2[1]; + axes[2] = axes2[2]; + + Object data = hdu.getData().getData(); + f.getStream().close(); + + if (data instanceof float[][][]) { + return float2double((float[][][]) data); + } else { // assume double[][][] + return (double[][][]) data; + } } - axes[0] = axes2[0]; - axes[1] = axes2[1]; - axes[2] = axes2[2]; - - Object data = hdu.getData().getData(); - f.getStream().close(); - - if (data instanceof float[][][]) { - return float2double((float[][][]) data); - } else {// assume double[][][] - return (double[][][]) data; - } - } - - /** - * Return image dimensions in this order: [numPlanes][iSize][jSize] - * - * @param filename - * @return - * @throws FitsException - * @throws IOException - */ - public static int[] getAxes(File filename) throws FitsException, IOException { - Fits f = new Fits(filename); - BasicHDU hdu = f.getHDU(0); - int[] axes = hdu.getAxes(); - f.close(); - return axes; - - } - - /** - * Extract plane from fits file. Throws error if file not found or if planeIndex is not in the - * number of planes of fits file. - * - * @param fitsFile - * @param planeIndex - specify using Java array indexing (counter starts at 0). - * @return - * @throws FitsException - * @throws IOException - */ - public static double[][] getPlaneFromFits(File fitsFile, int planeIndex, int[] axes) - throws FitsException, IOException { - - Fits f = new Fits(fitsFile); - BasicHDU hdu = f.getHDU(0); - axes = hdu.getAxes(); - if (axes.length != 3) { - throw new IOException( - "FITS file has incorrect dimensions! This was assumed to be 3D fits file!"); + /** + * Return image dimensions in this order: [numPlanes][iSize][jSize] + * + * @param filename + * @return + * @throws FitsException + * @throws IOException + */ + public static int[] getAxes(File filename) throws FitsException, IOException { + Fits f = new Fits(filename); + BasicHDU hdu = f.getHDU(0); + int[] axes = hdu.getAxes(); + f.close(); + return axes; } - // fits axes specifies image dimensions in this order: [numPlanes][iSize][jSize] - // check that planeIndex is valid - int numPlanes = axes[0]; - if ((planeIndex < 0)) { - String errMesg = "ERROR! Planeindex cannot be less than 0!"; - throw new RuntimeException(errMesg); + /** + * Extract plane from fits file. Throws error if file not found or if planeIndex is not in the + * number of planes of fits file. + * + * @param fitsFile + * @param planeIndex - specify using Java array indexing (counter starts at 0). + * @return + * @throws FitsException + * @throws IOException + */ + public static double[][] getPlaneFromFits(File fitsFile, int planeIndex, int[] axes) + throws FitsException, IOException { + + Fits f = new Fits(fitsFile); + BasicHDU hdu = f.getHDU(0); + axes = hdu.getAxes(); + if (axes.length != 3) { + throw new IOException("FITS file has incorrect dimensions! This was assumed to be 3D fits file!"); + } + + // fits axes specifies image dimensions in this order: [numPlanes][iSize][jSize] + // check that planeIndex is valid + int numPlanes = axes[0]; + if ((planeIndex < 0)) { + String errMesg = "ERROR! Planeindex cannot be less than 0!"; + throw new RuntimeException(errMesg); + } + + if ((planeIndex > numPlanes)) { + String errMesg = "ERROR! desired planeindex:" + planeIndex + " is greater than " + numPlanes + + " number of planes in file."; + throw new RuntimeException(errMesg); + } + + Object data = hdu.getData().getData(); + f.getStream().close(); + + double[][][] dataD = null; + if (data instanceof float[][][]) { + dataD = float2double((float[][][]) data); + } else { // assume double[][][] + dataD = (double[][][]) data; + } + double[][] planeData = new double[axes[1]][axes[2]]; + planeData = dataD[planeIndex]; + return planeData; } - if ((planeIndex > numPlanes)) { - String errMesg = "ERROR! desired planeindex:" + planeIndex + " is greater than " + numPlanes - + " number of planes in file."; - throw new RuntimeException(errMesg); - } + /** + * Find the row and column in the FITS data corresponding to an XYZ coordinate + * + * @param fitsData + * @param xyz + * @return 2D int array containing {row, col}. Row is second index into FITS cube, column is third + * index. This point will have magnitude within 1e-3 units and direction exactly the same + * as xyz. + */ + public static int[] findInFits(double[][][] fitsData, double[] xyz) { - Object data = hdu.getData().getData(); - f.getStream().close(); + // check xyz vector. If any components are NaN then return null; + for (int ii = 0; ii < 3; ii++) { + if (xyz[ii] == Double.NaN) { + return null; + } + } - double[][][] dataD = null; - if (data instanceof float[][][]) { - dataD = float2double((float[][][]) data); - } else {// assume double[][][] - dataD = (double[][][]) data; - } - double[][] planeData = new double[axes[1]][axes[2]]; - planeData = dataD[planeIndex]; - return planeData; - } + Vector3D xyzV = new Vector3D(xyz); + double mag2 = xyzV.getNorm(); - /** - * Find the row and column in the FITS data corresponding to an XYZ coordinate - * - * @param fitsData - * @param xyz - * @return 2D int array containing {row, col}. Row is second index into FITS cube, column is third - * index. This point will have magnitude within 1e-3 units and direction exactly the same - * as xyz. - */ - public static int[] findInFits(double[][][] fitsData, double[] xyz) { + for (int ii = 0; ii < fitsData[1].length; ii++) { + for (int jj = 0; jj < fitsData[0][1].length; jj++) { + Vector3D fits1P = new Vector3D(fitsData[4][ii][jj], fitsData[5][ii][jj], fitsData[6][ii][jj]); - // check xyz vector. If any components are NaN then return null; - for (int ii = 0; ii < 3; ii++) { - if (xyz[ii] == Double.NaN) { + // find angular separation between fits1 P vector and fits2 P vector + double radSep = Vector3D.angle(fits1P, xyzV); + + if (radSep == 0D) { + // compute magnitude of fits1 P vector and fits2 P vector. + double mag1 = fits1P.getNorm(); + double diffD = Math.abs(mag1 - mag2); + if (diffD < 0.001D) { + int[] rowCol = new int[2]; + rowCol[0] = ii; + rowCol[1] = jj; + return rowCol; + } + } + } + } return null; - } } - Vector3D xyzV = new Vector3D(xyz); - double mag2 = xyzV.getNorm(); + /** + * Returns true if HeaderTag is part of the general header keywords that all fits files should + * have. Used to filter out keywords that should be handled by the API and NOT set by the user. + * + * @param keyword + * @return + */ + public static boolean isGeneralHeaderKey(HeaderTag keyword) { + String name = keyword.name(); + switch (name) { + case "SIMPLE": + case "BITPIX": + case "NAXIS": + case "NAXIS1": + case "NAXIS2": + case "NAXIS3": + case "EXTEND": + return true; - for (int ii = 0; ii < fitsData[1].length; ii++) { - for (int jj = 0; jj < fitsData[0][1].length; jj++) { - Vector3D fits1P = - new Vector3D(fitsData[4][ii][jj], fitsData[5][ii][jj], fitsData[6][ii][jj]); - - // find angular separation between fits1 P vector and fits2 P vector - double radSep = Vector3D.angle(fits1P, xyzV); - - if (radSep == 0D) { - // compute magnitude of fits1 P vector and fits2 P vector. - double mag1 = fits1P.getNorm(); - double diffD = Math.abs(mag1 - mag2); - if (diffD < 0.001D) { - int[] rowCol = new int[2]; - rowCol[0] = ii; - rowCol[1] = jj; - return rowCol; - } + default: + return false; } - } - } - return null; - } - - /** - * Returns true if HeaderTag is part of the general header keywords that all fits files should - * have. Used to filter out keywords that should be handled by the API and NOT set by the user. - * - * @param keyword - * @return - */ - public static boolean isGeneralHeaderKey(HeaderTag keyword) { - String name = keyword.name(); - switch (name) { - - case "SIMPLE": - case "BITPIX": - case "NAXIS": - case "NAXIS1": - case "NAXIS2": - case "NAXIS3": - case "EXTEND": - return true; - - default: - return false; - } - } - - /** - * Generic method for creating a fits file with an N-plane image array and fits headers. - * - * @param data -contains N X M X P image array that will be written as a data cube of size N X M - * and P planes. - * @param outfile - * @param newHeaderCards - HeaderCards containing information for generating fits headers - * @throws FitsException - * @throws IOException - */ - public static void saveFits(double[][][] data, String outfile, List newHeaderCards) - throws FitsException, IOException { - float[][][] dataF = double2float(data); - - Fits f = new Fits(); - BasicHDU hdu = FitsFactory.hduFactory(dataF); - - // Add any tags passed in as the third argument of this function - if (newHeaderCards != null) { - for (HeaderCard hc : newHeaderCards) { - if (hc.getKey().toUpperCase().equals("COMMENT")) - hdu.getHeader().insertComment(hc.getComment()); - else - hdu.getHeader().addLine(hc); - } } - f.addHDU(hdu); - System.out.println("writing to:" + outfile); - BufferedFile bf = new BufferedFile(outfile, "rw"); - f.write(bf); - bf.close(); - f.close(); - } + /** + * Generic method for creating a fits file with an N-plane image array and fits headers. + * + * @param data -contains N X M X P image array that will be written as a data cube of size N X M + * and P planes. + * @param outfile + * @param newHeaderCards - HeaderCards containing information for generating fits headers + * @throws FitsException + * @throws IOException + */ + public static void saveFits(double[][][] data, String outfile, List newHeaderCards) + throws FitsException, IOException { + float[][][] dataF = double2float(data); - /** - * Generic method for creating a fits file with an 2D image array and fits headers. - * - * @param data -contains N X M image array that will be written as a 2D image of size N X M - * @param outfile - * @param newHeaderCards - HeaderCards containing information for generating fits headers - * @throws FitsException - * @throws IOException - */ - public static void save2DFits(double[][] data, String outfile, List newHeaderCards) - throws FitsException, IOException { + Fits f = new Fits(); + BasicHDU hdu = FitsFactory.hduFactory(dataF); - float[][] dataF = double2float2D(data); - - Fits f = new Fits(); - BasicHDU hdu = FitsFactory.hduFactory(dataF); - - // Add any tags passed in as the third argument of this function - if (newHeaderCards != null) { - for (HeaderCard hc : newHeaderCards) { - if (hc.getKey().toUpperCase().equals("COMMENT")) - hdu.getHeader().insertComment(hc.getComment()); - else - hdu.getHeader().addLine(hc); - } - } - - f.addHDU(hdu); - System.out.println("writing to:" + outfile); - BufferedFile bf = new BufferedFile(outfile, "rw"); - f.write(bf); - bf.close(); - f.close(); - } - - - /** - * Assuming data consists of multiple 2D planes with the following ordering: Note that this - * particular ordering is created so that the data is written properly to the fits file. - * double[][][] data = new double[numPlanes][iSize][jSize] - * - * Flip each 2D plane along the i dimension, i.e. do a vertical flip on each image. This is done - * because some fits readers have a default data orientation that is different from the fits - * writer orientation. - * - * The display orientation is defined by the PDS4 XML label but some applications may not be using - * the XML file to properly display the data cube. Hence it would be better to flip the data so - * the application displays it in the "correct" orientation. - * - * @param data - * @return - */ - public static double[][][] flipVertical(double[][][] data) { - - // retrieve lengths of each dimension - int numPlanes = data.length; - int numI = data[1].length; - int numJ = data[0][1].length; - - double[][][] newData = new double[numPlanes][numI][numJ]; - - for (int k = 0; k < numPlanes; k++) { - for (int i = 0; i < numI / 2; i++) { - for (int j = 0; j < numJ; j++) { - newData[k][i][j] = data[k][numI - i - 1][j]; - newData[k][numI - i - 1][j] = data[k][i][j]; + // Add any tags passed in as the third argument of this function + if (newHeaderCards != null) { + for (HeaderCard hc : newHeaderCards) { + if (hc.getKey().toUpperCase().equals("COMMENT")) hdu.getHeader().insertComment(hc.getComment()); + else hdu.getHeader().addLine(hc); + } } - } + + f.addHDU(hdu); + System.out.println("writing to:" + outfile); + BufferedFile bf = new BufferedFile(outfile, "rw"); + f.write(bf); + bf.close(); + f.close(); } - return newData; - } - public static double[][][] xyz2llrxyz(double[][][] indata) { - int numPlanes = indata.length; - if (numPlanes != 3) { - System.out.println("Error: cube must contain exactly 3 planes"); - return null; - } - int numRows = indata[0].length; - int numCols = indata[0][0].length; - double[][][] outdata = new double[6][numRows][numCols]; - double[] pt = new double[3]; - for (int i = 0; i < numRows; ++i) - for (int j = 0; j < numCols; ++j) { - pt[0] = indata[0][i][j]; - pt[1] = indata[1][i][j]; - pt[2] = indata[2][i][j]; + /** + * Generic method for creating a fits file with an 2D image array and fits headers. + * + * @param data -contains N X M image array that will be written as a 2D image of size N X M + * @param outfile + * @param newHeaderCards - HeaderCards containing information for generating fits headers + * @throws FitsException + * @throws IOException + */ + public static void save2DFits(double[][] data, String outfile, List newHeaderCards) + throws FitsException, IOException { - Vector3D v = new Vector3D(pt); + float[][] dataF = double2float2D(data); - outdata[0][i][j] = Math.toDegrees(v.getAlpha()); - outdata[1][i][j] = Math.toDegrees(v.getDelta()); - outdata[2][i][j] = v.getNorm(); - outdata[3][i][j] = pt[0]; - outdata[4][i][j] = pt[1]; - outdata[5][i][j] = pt[2]; - } - return outdata; - } + Fits f = new Fits(); + BasicHDU hdu = FitsFactory.hduFactory(dataF); - public static double[][][] llr2llrxyz(double[][][] indata) { - int numPlanes = indata.length; - if (numPlanes != 3) { - System.out.println("Error: cube must contain exactly 3 planes"); - return null; - } - int numRows = indata[0].length; - int numCols = indata[0][0].length; - double[][][] outdata = new double[6][numRows][numCols]; - for (int i = 0; i < numRows; ++i) - for (int j = 0; j < numCols; ++j) { - double lat = indata[0][i][j]; - double lon = indata[1][i][j]; - double rad = indata[2][i][j]; - - double[] pt = - new Vector3D(Math.toRadians(lon), Math.toRadians(lat)).scalarMultiply(rad).toArray(); - - outdata[0][i][j] = indata[0][i][j]; - outdata[1][i][j] = indata[1][i][j]; - outdata[2][i][j] = indata[2][i][j]; - outdata[3][i][j] = pt[0]; - outdata[4][i][j] = pt[1]; - outdata[5][i][j] = pt[2]; - } - return outdata; - } - - public static float[][][] double2float(double[][][] indata) { - int numPlanes = indata.length; - int numRows = indata[0].length; - int numCols = indata[0][0].length; - float[][][] outdata = new float[numPlanes][numRows][numCols]; - for (int k = 0; k < numPlanes; ++k) - for (int i = 0; i < numRows; ++i) - for (int j = 0; j < numCols; ++j) { - outdata[k][i][j] = (float) indata[k][i][j]; + // Add any tags passed in as the third argument of this function + if (newHeaderCards != null) { + for (HeaderCard hc : newHeaderCards) { + if (hc.getKey().toUpperCase().equals("COMMENT")) hdu.getHeader().insertComment(hc.getComment()); + else hdu.getHeader().addLine(hc); + } } - return outdata; - } - public static float[][] double2float2D(double[][] indata) { - int numRows = indata.length; - int numCols = indata[0].length; - float[][] outdata = new float[numRows][numCols]; - for (int i = 0; i < numRows; ++i) - for (int j = 0; j < numCols; ++j) { - outdata[i][j] = (float) indata[i][j]; - } - return outdata; - } + f.addHDU(hdu); + System.out.println("writing to:" + outfile); + BufferedFile bf = new BufferedFile(outfile, "rw"); + f.write(bf); + bf.close(); + f.close(); + } - public static double[][][] float2double(float[][][] indata) { - int numPlanes = indata.length; - int numRows = indata[0].length; - int numCols = indata[0][0].length; - double[][][] outdata = new double[numPlanes][numRows][numCols]; - for (int k = 0; k < numPlanes; ++k) - for (int i = 0; i < numRows; ++i) - for (int j = 0; j < numCols; ++j) { - outdata[k][i][j] = indata[k][i][j]; + /** + * Assuming data consists of multiple 2D planes with the following ordering: Note that this + * particular ordering is created so that the data is written properly to the fits file. + * double[][][] data = new double[numPlanes][iSize][jSize] + * + * Flip each 2D plane along the i dimension, i.e. do a vertical flip on each image. This is done + * because some fits readers have a default data orientation that is different from the fits + * writer orientation. + * + * The display orientation is defined by the PDS4 XML label but some applications may not be using + * the XML file to properly display the data cube. Hence it would be better to flip the data so + * the application displays it in the "correct" orientation. + * + * @param data + * @return + */ + public static double[][][] flipVertical(double[][][] data) { + + // retrieve lengths of each dimension + int numPlanes = data.length; + int numI = data[1].length; + int numJ = data[0][1].length; + + double[][][] newData = new double[numPlanes][numI][numJ]; + + for (int k = 0; k < numPlanes; k++) { + for (int i = 0; i < numI / 2; i++) { + for (int j = 0; j < numJ; j++) { + newData[k][i][j] = data[k][numI - i - 1][j]; + newData[k][numI - i - 1][j] = data[k][i][j]; + } + } } - return outdata; - } + return newData; + } - public static double[][][] crop(double[][][] indata, int cropAmount) { - int numPlanes = indata.length; - int numRows = indata[0].length; - int numCols = indata[0][0].length; - double[][][] outdata = - new double[numPlanes][numRows - 2 * cropAmount][numCols - 2 * cropAmount]; - for (int k = 0; k < numPlanes; ++k) - for (int i = 0; i < numRows - 2 * cropAmount; ++i) - for (int j = 0; j < numCols - 2 * cropAmount; ++j) { - outdata[k][i][j] = indata[k][i + cropAmount][j + cropAmount]; + public static double[][][] xyz2llrxyz(double[][][] indata) { + int numPlanes = indata.length; + if (numPlanes != 3) { + System.out.println("Error: cube must contain exactly 3 planes"); + return null; } - return outdata; - } + int numRows = indata[0].length; + int numCols = indata[0][0].length; + double[][][] outdata = new double[6][numRows][numCols]; + double[] pt = new double[3]; + for (int i = 0; i < numRows; ++i) + for (int j = 0; j < numCols; ++j) { + pt[0] = indata[0][i][j]; + pt[1] = indata[1][i][j]; + pt[2] = indata[2][i][j]; - /** - * Update a single HeaderCard in a list of HeaderCards. Search on the keyword in the list and - * replace with the new HeaderCard. This method has been updated to accept a HeaderCard as input - * as this is the most generalized form. This allows the datatype of the HeaderCard value to be - * preserved or even to change. I.e. the original HeaderCard value could be a double and this lets - * one change the datatype to a string. NOTE: Does NOTHING if keyword is not found in the list! To - * append the keyword when it is not found in the list, use updateOrAppendCard(). - * - * @param headers - * @param newHeaderCard - * @return - */ - public static List updateCard(List headers, HeaderCard newHeaderCard) { + Vector3D v = new Vector3D(pt); - List newHeaders = new ArrayList(); - - // find kitsKey in the list. - for (HeaderCard thisCard : headers) { - if ((newHeaderCard.getKey()).equals(thisCard.getKey())) { - newHeaders.add(newHeaderCard); - } else { - newHeaders.add(thisCard); - } + outdata[0][i][j] = Math.toDegrees(v.getAlpha()); + outdata[1][i][j] = Math.toDegrees(v.getDelta()); + outdata[2][i][j] = v.getNorm(); + outdata[3][i][j] = pt[0]; + outdata[4][i][j] = pt[1]; + outdata[5][i][j] = pt[2]; + } + return outdata; } - return newHeaders; - } + public static double[][][] llr2llrxyz(double[][][] indata) { + int numPlanes = indata.length; + if (numPlanes != 3) { + System.out.println("Error: cube must contain exactly 3 planes"); + return null; + } + int numRows = indata[0].length; + int numCols = indata[0][0].length; + double[][][] outdata = new double[6][numRows][numCols]; + for (int i = 0; i < numRows; ++i) + for (int j = 0; j < numCols; ++j) { + double lat = indata[0][i][j]; + double lon = indata[1][i][j]; + double rad = indata[2][i][j]; - /** - * Update a single HeaderCard in a list of HeaderCards. Search on the keyword in the list and - * replace with the new value and new comment(Optional. Comment not updated if comment string is - * null). If keyword does NOT exist in the list then append it to the list. - * - * @param headers - * @param newHeaderCard - * @return - */ - public static List updateOrAppendCard(List headers, - HeaderCard newHeaderCard) { + double[] pt = new Vector3D(Math.toRadians(lon), Math.toRadians(lat)) + .scalarMultiply(rad) + .toArray(); - List newHeaders = new ArrayList(); - - // find kitsKey in the list. - boolean cardFound = false; - for (HeaderCard thisCard : headers) { - if ((newHeaderCard.getKey()).equals(thisCard.getKey())) { - newHeaders.add(newHeaderCard); - cardFound = true; - } else { - newHeaders.add(thisCard); - } + outdata[0][i][j] = indata[0][i][j]; + outdata[1][i][j] = indata[1][i][j]; + outdata[2][i][j] = indata[2][i][j]; + outdata[3][i][j] = pt[0]; + outdata[4][i][j] = pt[1]; + outdata[5][i][j] = pt[2]; + } + return outdata; } - if (!cardFound) { - newHeaders.add(newHeaderCard); + public static float[][][] double2float(double[][][] indata) { + int numPlanes = indata.length; + int numRows = indata[0].length; + int numCols = indata[0][0].length; + float[][][] outdata = new float[numPlanes][numRows][numCols]; + for (int k = 0; k < numPlanes; ++k) + for (int i = 0; i < numRows; ++i) + for (int j = 0; j < numCols; ++j) { + outdata[k][i][j] = (float) indata[k][i][j]; + } + return outdata; } - return newHeaders; - - } - - - /** - * Return the fits header as List<HeaderCard>. Each HeaderCard is equivalent to the FITS - * Keyword = Value, comment line in a fits header. - * - * @param fitsFile - * @return - * @throws FitsException - * @throws IOException - */ - public static List getFitsHeader(String fitsFile) throws FitsException, IOException { - Fits inf = new Fits(fitsFile); - BasicHDU inHdu = inf.getHDU(0); - // Object data = inhdu.getData().getData(); - List inHeaders = new ArrayList(); - Cursor cursor = inHdu.getHeader().iterator(); - while (cursor.hasNext()) { - HeaderCard hc = (HeaderCard) cursor.next(); - inHeaders.add(hc); + public static float[][] double2float2D(double[][] indata) { + int numRows = indata.length; + int numCols = indata[0].length; + float[][] outdata = new float[numRows][numCols]; + for (int i = 0; i < numRows; ++i) + for (int j = 0; j < numCols; ++j) { + outdata[i][j] = (float) indata[i][j]; + } + return outdata; } - inf.getStream().close(); - return inHeaders; - } - - /** - * Return headercard in the fits header map given the key. Return null if headercard not found, - * unless boolean failOnNull = true. If true, then method will throw a FitsException if headerCard - * not found. - * - * @param map - * @param searchKey - * @param failOnNull - * @return - * @throws FitsException - */ - public static HeaderCard getCard(Map map, String searchKey, - boolean failOnNull) throws FitsException { - HeaderCard returnCard = null; - if (map.containsKey(searchKey)) { - returnCard = map.get(searchKey); - } else { - if (failOnNull) { - String error = "ERROR! Could not parse fits Keyword:" + searchKey; - throw new FitsException(error); - } else { - return null; - } - } - return returnCard; - } - - /** - * Load fits header into Map<String, HeaderCard> where String is the string representation - * of the fits keyword associated with the headerCard. - * - * @param fitsFile - * @return - * @throws FitsException - * @throws IOException - */ - public static Map getFitsHeaderAsMap(String fitsFile) - throws FitsException, IOException { - Fits inf = new Fits(fitsFile); - BasicHDU inHdu = inf.getHDU(0); - - // preserve order of keywords loaded from fits file - Map inHeaders = new LinkedHashMap(); - Cursor cursor = inHdu.getHeader().iterator(); - while (cursor.hasNext()) { - HeaderCard hc = (HeaderCard) cursor.next(); - inHeaders.put(hc.getKey(), hc); + public static double[][][] float2double(float[][][] indata) { + int numPlanes = indata.length; + int numRows = indata[0].length; + int numCols = indata[0][0].length; + double[][][] outdata = new double[numPlanes][numRows][numCols]; + for (int k = 0; k < numPlanes; ++k) + for (int i = 0; i < numRows; ++i) + for (int j = 0; j < numCols; ++j) { + outdata[k][i][j] = indata[k][i][j]; + } + return outdata; } - inf.getStream().close(); - inf.close(); - return inHeaders; - } - - /** - * Generate a list of HeaderCards given a map. Useful for transforming a map of keywords loaded - * from input file into a list of keywords for a fits output file. Warning: the list will be in - * random order if the map class used did not preserve insertion order. - * - * @param map - * @return - */ - public static List map2HeaderCards(Map map) { - List headerCards = new ArrayList(); - for (String key : map.keySet()) { - headerCards.add(map.get(key)); + public static double[][][] crop(double[][][] indata, int cropAmount) { + int numPlanes = indata.length; + int numRows = indata[0].length; + int numCols = indata[0][0].length; + double[][][] outdata = new double[numPlanes][numRows - 2 * cropAmount][numCols - 2 * cropAmount]; + for (int k = 0; k < numPlanes; ++k) + for (int i = 0; i < numRows - 2 * cropAmount; ++i) + for (int j = 0; j < numCols - 2 * cropAmount; ++j) { + outdata[k][i][j] = indata[k][i + cropAmount][j + cropAmount]; + } + return outdata; } - return headerCards; - } - /** - * Return the Fits Table Header Data Unit given the path to a fits table file. Does not matter - * whether table is Fits binary or ASCII. - * - * @throws IOException - * @throws FitsException - */ - public static TableHDU TableHDUofFile(String fitsFile) throws FitsException, IOException { - System.out.println("fits file:" + fitsFile); - Fits tableFits = new Fits(fitsFile); - int numHDUs = tableFits.getNumberOfHDUs(); - System.out.println("number of hdus:" + String.valueOf(numHDUs)); - TableHDU tableHDU = (TableHDU) tableFits.getHDU(1); + /** + * Update a single HeaderCard in a list of HeaderCards. Search on the keyword in the list and + * replace with the new HeaderCard. This method has been updated to accept a HeaderCard as input + * as this is the most generalized form. This allows the datatype of the HeaderCard value to be + * preserved or even to change. I.e. the original HeaderCard value could be a double and this lets + * one change the datatype to a string. NOTE: Does NOTHING if keyword is not found in the list! To + * append the keyword when it is not found in the list, use updateOrAppendCard(). + * + * @param headers + * @param newHeaderCard + * @return + */ + public static List updateCard(List headers, HeaderCard newHeaderCard) { - int numRows = tableHDU.getNRows(); - int numCols = tableHDU.getNCols(); + List newHeaders = new ArrayList(); - System.out.println("numRows:" + String.valueOf(numRows)); - System.out.println("numCols:" + String.valueOf(numCols)); + // find kitsKey in the list. + for (HeaderCard thisCard : headers) { + if ((newHeaderCard.getKey()).equals(thisCard.getKey())) { + newHeaders.add(newHeaderCard); + } else { + newHeaders.add(thisCard); + } + } - return tableHDU; - } + return newHeaders; + } + + /** + * Update a single HeaderCard in a list of HeaderCards. Search on the keyword in the list and + * replace with the new value and new comment(Optional. Comment not updated if comment string is + * null). If keyword does NOT exist in the list then append it to the list. + * + * @param headers + * @param newHeaderCard + * @return + */ + public static List updateOrAppendCard(List headers, HeaderCard newHeaderCard) { + + List newHeaders = new ArrayList(); + + // find kitsKey in the list. + boolean cardFound = false; + for (HeaderCard thisCard : headers) { + if ((newHeaderCard.getKey()).equals(thisCard.getKey())) { + newHeaders.add(newHeaderCard); + cardFound = true; + } else { + newHeaders.add(thisCard); + } + } + + if (!cardFound) { + newHeaders.add(newHeaderCard); + } + + return newHeaders; + } + + /** + * Return the fits header as List<HeaderCard>. Each HeaderCard is equivalent to the FITS + * Keyword = Value, comment line in a fits header. + * + * @param fitsFile + * @return + * @throws FitsException + * @throws IOException + */ + public static List getFitsHeader(String fitsFile) throws FitsException, IOException { + Fits inf = new Fits(fitsFile); + BasicHDU inHdu = inf.getHDU(0); + // Object data = inhdu.getData().getData(); + List inHeaders = new ArrayList(); + Cursor cursor = inHdu.getHeader().iterator(); + while (cursor.hasNext()) { + HeaderCard hc = (HeaderCard) cursor.next(); + inHeaders.add(hc); + } + + inf.getStream().close(); + return inHeaders; + } + + /** + * Return headercard in the fits header map given the key. Return null if headercard not found, + * unless boolean failOnNull = true. If true, then method will throw a FitsException if headerCard + * not found. + * + * @param map + * @param searchKey + * @param failOnNull + * @return + * @throws FitsException + */ + public static HeaderCard getCard(Map map, String searchKey, boolean failOnNull) + throws FitsException { + HeaderCard returnCard = null; + if (map.containsKey(searchKey)) { + returnCard = map.get(searchKey); + } else { + if (failOnNull) { + String error = "ERROR! Could not parse fits Keyword:" + searchKey; + throw new FitsException(error); + } else { + return null; + } + } + return returnCard; + } + + /** + * Load fits header into Map<String, HeaderCard> where String is the string representation + * of the fits keyword associated with the headerCard. + * + * @param fitsFile + * @return + * @throws FitsException + * @throws IOException + */ + public static Map getFitsHeaderAsMap(String fitsFile) throws FitsException, IOException { + Fits inf = new Fits(fitsFile); + BasicHDU inHdu = inf.getHDU(0); + + // preserve order of keywords loaded from fits file + Map inHeaders = new LinkedHashMap(); + Cursor cursor = inHdu.getHeader().iterator(); + while (cursor.hasNext()) { + HeaderCard hc = (HeaderCard) cursor.next(); + inHeaders.put(hc.getKey(), hc); + } + + inf.getStream().close(); + inf.close(); + return inHeaders; + } + + /** + * Generate a list of HeaderCards given a map. Useful for transforming a map of keywords loaded + * from input file into a list of keywords for a fits output file. Warning: the list will be in + * random order if the map class used did not preserve insertion order. + * + * @param map + * @return + */ + public static List map2HeaderCards(Map map) { + List headerCards = new ArrayList(); + for (String key : map.keySet()) { + headerCards.add(map.get(key)); + } + return headerCards; + } + + /** + * Return the Fits Table Header Data Unit given the path to a fits table file. Does not matter + * whether table is Fits binary or ASCII. + * + * @throws IOException + * @throws FitsException + */ + public static TableHDU TableHDUofFile(String fitsFile) throws FitsException, IOException { + System.out.println("fits file:" + fitsFile); + Fits tableFits = new Fits(fitsFile); + int numHDUs = tableFits.getNumberOfHDUs(); + System.out.println("number of hdus:" + String.valueOf(numHDUs)); + TableHDU tableHDU = (TableHDU) tableFits.getHDU(1); + + int numRows = tableHDU.getNRows(); + int numCols = tableHDU.getNCols(); + + System.out.println("numRows:" + String.valueOf(numRows)); + System.out.println("numCols:" + String.valueOf(numCols)); + + return tableHDU; + } } diff --git a/src/main/java/terrasaur/fits/FitsValCom.java b/src/main/java/terrasaur/fits/FitsValCom.java index 9eb2cf0..8aeba8f 100644 --- a/src/main/java/terrasaur/fits/FitsValCom.java +++ b/src/main/java/terrasaur/fits/FitsValCom.java @@ -24,38 +24,37 @@ package terrasaur.fits; /** * Container class for storing the value and comment associated with a given fits keyword - * + * * @author espirrc1 * */ public class FitsValCom { - private String value; - private String comment; + private String value; + private String comment; - public FitsValCom(String value, String comment) { - this.value = value; - this.comment = comment; - } + public FitsValCom(String value, String comment) { + this.value = value; + this.comment = comment; + } - public String getV() { - return value; - } + public String getV() { + return value; + } - public String getC() { - return comment; - } + public String getC() { + return comment; + } - public void setV(String newVal) { - this.value = newVal; - } + public void setV(String newVal) { + this.value = newVal; + } - public void setC(String newComment) { - this.comment = newComment; - } - - public void setVC(String newVal, String newComment) { - setV(newVal); - setC(newComment); - } + public void setC(String newComment) { + this.comment = newComment; + } + public void setVC(String newVal, String newComment) { + setV(newVal); + setC(newComment); + } } diff --git a/src/main/java/terrasaur/fits/GenericAnciGlobal.java b/src/main/java/terrasaur/fits/GenericAnciGlobal.java index c8b8d43..22b1fb5 100644 --- a/src/main/java/terrasaur/fits/GenericAnciGlobal.java +++ b/src/main/java/terrasaur/fits/GenericAnciGlobal.java @@ -31,38 +31,36 @@ import terrasaur.enums.FitsHeaderType; /** * Contains methods for building fits header corresponding to the Generic Ancillary GLobal fits * header as specified in the Map Formats SIS. - * + * * See concrete methods and attributes in AnciTableFits unless overridden. Overridden or * implementation methods specific to this fits type are here. - * + * * @author espirrc1 * */ public class GenericAnciGlobal extends AnciTableFits implements AnciFitsHeader { - public GenericAnciGlobal(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.ANCIGLOBALGENERIC); - } - - - /** - * Build fits header portion that contains the spatial information of the Generic Anci Global - * product. Overrides the default implementation in AnciTableFits. - */ - @Override - public List getSpatialInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + public GenericAnciGlobal(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.ANCIGLOBALGENERIC); } - // the generic Global Anci product ONLY CONTAINS THE CENTER LAT LON! - // per map_format_fits_header_normalization_09212917_V02.xlsx - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + /** + * Build fits header portion that contains the spatial information of the Generic Anci Global + * product. Overrides the default implementation in AnciTableFits. + */ + @Override + public List getSpatialInfo(String comment) throws HeaderCardException { - return headers; - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + // the generic Global Anci product ONLY CONTAINS THE CENTER LAT LON! + // per map_format_fits_header_normalization_09212917_V02.xlsx + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/GenericAnciLocal.java b/src/main/java/terrasaur/fits/GenericAnciLocal.java index ab0b7b7..198ea13 100644 --- a/src/main/java/terrasaur/fits/GenericAnciLocal.java +++ b/src/main/java/terrasaur/fits/GenericAnciLocal.java @@ -27,18 +27,16 @@ import terrasaur.enums.FitsHeaderType; /** * Contains methods for building fits header corresponding to the Generic Ancillary Local fits * header as specified in the Map Formats SIS. - * + * * See concrete methods and attributes in AnciTableFits unless overridden. Overridden or * implementation methods specific to this fits type are here. - * + * * @author espirrc1 * */ public class GenericAnciLocal extends AnciTableFits implements AnciFitsHeader { - public GenericAnciLocal(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.ANCILOCALGENERIC); - } - - + public GenericAnciLocal(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.ANCILOCALGENERIC); + } } diff --git a/src/main/java/terrasaur/fits/GenericGlobalDTM.java b/src/main/java/terrasaur/fits/GenericGlobalDTM.java index fe715ee..908d2b6 100644 --- a/src/main/java/terrasaur/fits/GenericGlobalDTM.java +++ b/src/main/java/terrasaur/fits/GenericGlobalDTM.java @@ -28,14 +28,13 @@ import terrasaur.utils.DTMHeader; /** * Contains methods for building generic Global DTM fits header. Generic Global DTM header will * contain only those keywords that are deemed common to all Global DTM fits files. - * + * * @author espirrc1 * */ public class GenericGlobalDTM extends DTMFits implements DTMHeader { - public GenericGlobalDTM(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.DTMGLOBALGENERIC); - } - + public GenericGlobalDTM(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.DTMGLOBALGENERIC); + } } diff --git a/src/main/java/terrasaur/fits/GenericLocalDTM.java b/src/main/java/terrasaur/fits/GenericLocalDTM.java index ee8cb63..3f01163 100644 --- a/src/main/java/terrasaur/fits/GenericLocalDTM.java +++ b/src/main/java/terrasaur/fits/GenericLocalDTM.java @@ -28,14 +28,13 @@ import terrasaur.utils.DTMHeader; /** * Contains methods for building generic Local DTM Fits Header. DTM header will contain only those * keywords that are deemed common to all Local DTM fits files. - * + * * @author espirrc1 * */ public class GenericLocalDTM extends DTMFits implements DTMHeader { - public GenericLocalDTM(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.DTMLOCALGENERIC); - } - + public GenericLocalDTM(FitsHdr fitsHeader) { + super(fitsHeader, FitsHeaderType.DTMLOCALGENERIC); + } } diff --git a/src/main/java/terrasaur/fits/HeaderTag.java b/src/main/java/terrasaur/fits/HeaderTag.java index b2efb85..4a09c2c 100644 --- a/src/main/java/terrasaur/fits/HeaderTag.java +++ b/src/main/java/terrasaur/fits/HeaderTag.java @@ -31,153 +31,177 @@ import java.util.EnumSet; * enumeration. A few keywords may have overlap. For example, SIGMA is defined here to represent the * global uncertainty of the data. It is also in PlaneInfo to define an entire image plane * consisting of sigma values. - * + * * @author espirrc1 - * + * */ public enum HeaderTag { - // fits data tags. Included here in case we want to have values or comments - // that are not null. Some of the default values obviously need to be updated when actual fits - // file - // is created. - SIMPLE("T", null), BITPIX(null, null), NAXIS("3", null), NAXIS1(null, null), NAXIS2(null, null), + // fits data tags. Included here in case we want to have values or comments + // that are not null. Some of the default values obviously need to be updated when actual fits + // file + // is created. + SIMPLE("T", null), + BITPIX(null, null), + NAXIS("3", null), + NAXIS1(null, null), + NAXIS2(null, null), - NAXIS3(null, null), EXTEND("T", null), HDRVERS("1.0.0", null), NPRDVERS("1.0.0", - null), MISSION("OSIRIS-REx", "Name of mission"), HOSTNAME("OREX", "PDS ID"), + NAXIS3(null, null), + EXTEND("T", null), + HDRVERS("1.0.0", null), + NPRDVERS("1.0.0", null), + MISSION("OSIRIS-REx", "Name of mission"), + HOSTNAME("OREX", "PDS ID"), - TARGET("101955 BENNU", "Target object"), ORIGIN("OREXSPOC", null), + TARGET("101955 BENNU", "Target object"), + ORIGIN("OREXSPOC", null), - SPOC_ID("SPOCUPLOAD", null), SDPAREA("SPOCUPLOAD", null), SDPDESC("SPOCUPLOAD", null), + SPOC_ID("SPOCUPLOAD", null), + SDPAREA("SPOCUPLOAD", null), + SDPDESC("SPOCUPLOAD", null), - MPHASE("FillMeIn", "Mission phase."), DATASRC("FillMeIn", - "Shape model data source, i.e. 'SPC' or 'OLA'"), + MPHASE("FillMeIn", "Mission phase."), + DATASRC("FillMeIn", "Shape model data source, i.e. 'SPC' or 'OLA'"), - DATASRCF("FillMeIn", "Source shape model data filename"), DATASRCV("FillMeIn", - "Name and version of shape model"), DATASRCD("FillMeIn", - "Creation date of shape model in UTC"), DATASRCS("N/A", - "[m/pix] Shpe model plt scale"), SOFTWARE("FillMeIn", - "Software used to create map data"), + DATASRCF("FillMeIn", "Source shape model data filename"), + DATASRCV("FillMeIn", "Name and version of shape model"), + DATASRCD("FillMeIn", "Creation date of shape model in UTC"), + DATASRCS("N/A", "[m/pix] Shpe model plt scale"), + SOFTWARE("FillMeIn", "Software used to create map data"), - SOFT_VER("FillMeIn", "Version of software used to create map data"), + SOFT_VER("FillMeIn", "Version of software used to create map data"), + DATEPRD("1701-10-09", "Date this product was produced in UTC"), + DATENPRD("1701-10-09", "Date this NFT product was produced in UTC"), + PRODNAME("FillMeIn", "Product filename"), + PRODVERS("1.0.0", "Product version number"), - DATEPRD("1701-10-09", "Date this product was produced in UTC"), DATENPRD("1701-10-09", - "Date this NFT product was produced in UTC"), PRODNAME("FillMeIn", - "Product filename"), PRODVERS("1.0.0", "Product version number"), + MAP_VER("999", "Product version number."), + CREATOR("ALT-pipeline", "Name of software that created this product"), + AUTHOR("Espiritu", "Name of person that compiled this product"), + PROJECTN("Equirectangular", "Simple cylindrical projection"), + CLON("-999", "[deg] longitude at center of image"), + CLAT("-999", "[deg] latitude at center of image"), + MINLON(null, "[deg] minimum longitude of global DTM"), + MAXLON(null, "[deg] maximum longitude of global DTM"), + MINLAT(null, "[deg] minimum latitude of global DTM"), + MAXLAT(null, "[deg] maximum latitude of global DTM"), - MAP_VER("999", "Product version number."), CREATOR("ALT-pipeline", - "Name of software that created this product"), AUTHOR("Espiritu", - "Name of person that compiled this product"), PROJECTN("Equirectangular", - "Simple cylindrical projection"), CLON("-999", - "[deg] longitude at center of image"), CLAT("-999", - "[deg] latitude at center of image"), MINLON(null, - "[deg] minimum longitude of global DTM"), MAXLON(null, - "[deg] maximum longitude of global DTM"), MINLAT(null, - "[deg] minimum latitude of global DTM"), MAXLAT(null, - "[deg] maximum latitude of global DTM"), + PXPERDEG("-999", "[pixel per degree] grid spacing of global map."), + LLCLAT("-999", "[deg]"), + LLCLNG("-999", "[deg]"), + ULCLAT("-999", "[deg]"), + ULCLNG("-999", "[deg]"), - PXPERDEG("-999", "[pixel per degree] grid spacing of global map."), LLCLAT("-999", - "[deg]"), LLCLNG("-999", "[deg]"), ULCLAT("-999", "[deg]"), ULCLNG("-999", "[deg]"), + URCLAT("-999", "[deg]"), + URCLNG("-999", "[deg]"), + LRCLAT("-999", "[deg]"), + LRCLNG("-999", "[deg]"), - URCLAT("-999", "[deg]"), URCLNG("-999", "[deg]"), LRCLAT("-999", "[deg]"), LRCLNG("-999", - "[deg]"), + CNTR_V_X("-999", "[km]"), + CNTR_V_Y("-999", "[km]"), + CNTR_V_Z("-999", "[km]"), + UX_X("-999", "[m]"), + UX_Y("-999", "[m]"), - CNTR_V_X("-999", "[km]"), CNTR_V_Y("-999", "[km]"), CNTR_V_Z("-999", "[km]"), UX_X("-999", - "[m]"), UX_Y("-999", "[m]"), + UX_Z("-999", "[m]"), + UY_X("-999", "[m]"), + UY_Y("-999", "[m]"), + UY_Z("-999", "[m]"), + UZ_X("-999", "[m]"), - UX_Z("-999", "[m]"), UY_X("-999", "[m]"), UY_Y("-999", "[m]"), UY_Z("-999", "[m]"), UZ_X("-999", - "[m]"), + UZ_Y("-999", "/[m]"), + UZ_Z("-999", "[m]"), + GSD("-999", "[mm] grid spacing in units/pixel"), + GSDI("-999", "[unk] Ground sample distance integer"), + SIGMA("-999", "Global uncertainty of the data [m]"), - UZ_Y("-999", "/[m]"), UZ_Z("-999", "[m]"), GSD("-999", "[mm] grid spacing in units/pixel"), GSDI( - "-999", - "[unk] Ground sample distance integer"), SIGMA("-999", "Global uncertainty of the data [m]"), + SIG_DEF("Uncertainty", "SIGMA uncertainty metric"), + DQUAL_1("-999", "Data quality metric; incidence directions"), - SIG_DEF("Uncertainty", "SIGMA uncertainty metric"), DQUAL_1("-999", - "Data quality metric; incidence directions"), + DQUAL_2("-999", "Data quality metric; emission directions"), + DSIG_DEF("UNK", "Defines uncertainty metric in ancillary file"), + END(null, null), - DQUAL_2("-999", "Data quality metric; emission directions"), DSIG_DEF("UNK", - "Defines uncertainty metric in ancillary file"), END(null, null), + // additional fits tags describing gravity derived values + DENSITY("-999", "[kgm^-3] density of body"), + ROT_RATE("-999", "[rad/sec] rotation rate of body"), + REF_POT("-999", "[J/kg] reference potential of body"), - // additional fits tags describing gravity derived values - DENSITY("-999", "[kgm^-3] density of body"), ROT_RATE("-999", - "[rad/sec] rotation rate of body"), REF_POT("-999", "[J/kg] reference potential of body"), + // additional fits tags describing tilt derived values + TILT_RAD("-999", "[m]"), + TILT_MAJ("-999", "[m] semi-major axis of ellipse for tilt calcs"), + TILT_MIN("-999", "[m] semi-minor axis of ellipse for tilt calcs"), + TILT_PA("-999", "[deg] position angle of ellipse for tilt calcs"), - // additional fits tags describing tilt derived values - TILT_RAD("-999", "[m]"), TILT_MAJ("-999", - "[m] semi-major axis of ellipse for tilt calcs"), TILT_MIN("-999", - "[m] semi-minor axis of ellipse for tilt calcs"), TILT_PA("-999", - "[deg] position angle of ellipse for tilt calcs"), + // Additional fits tags specific to ancillary fits files + MAP_NAME("FillMeIn", "Map data type"), + MAP_TYPE("FillMeIn", "Defines whether this is a global or local map"), + OBJ_FILE("TEMPLATEENTRY", null), - // Additional fits tags specific to ancillary fits files - MAP_NAME("FillMeIn", "Map data type"), MAP_TYPE("FillMeIn", - "Defines whether this is a global or local map"), OBJ_FILE("TEMPLATEENTRY", null), + // keywords specific to facet mapping ancillary file + OBJINDX("UNK", "OBJ indexed to OBJ_FILE"), + GSDINDX("-999", "[unk] Ground sample distance of OBJINDX"), + GSDINDXI("-999", "[unk] GSDINDX integer"), - // keywords specific to facet mapping ancillary file - OBJINDX("UNK", "OBJ indexed to OBJ_FILE"), GSDINDX("-999", - "[unk] Ground sample distance of OBJINDX"), GSDINDXI("-999", "[unk] GSDINDX integer"), + // return this enum when no match is found + NOMATCH(null, "could not determine"); - // return this enum when no match is found - NOMATCH(null, "could not determine"); + // PIXPDEG(null,"pixels per degree","pixel/deg"), + // PIX_SZ(null, "mean size of pixels at equator (meters)","m"); - // PIXPDEG(null,"pixels per degree","pixel/deg"), - // PIX_SZ(null, "mean size of pixels at equator (meters)","m"); + private FitsValCom fitsValCom; - - private FitsValCom fitsValCom; - - private HeaderTag(String value, String comment) { - this.fitsValCom = new FitsValCom(value, comment); - } - - public String value() { - return this.fitsValCom.getV(); - } - - public String comment() { - return this.fitsValCom.getC(); - } - - /** - * Contains all valid Fits keywords for this Enum. 'NOMATCH' is not a valid Fits keyword - */ - public static final EnumSet fitsKeywords = - EnumSet.range(HeaderTag.SIMPLE, HeaderTag.GSDINDXI); - - public static final EnumSet globalDTMFitsData = - EnumSet.of(HeaderTag.CLAT, HeaderTag.CLON); - - - /** - * Return the HeaderTag associated with a given string. returns NOMATCH enum if no match found. - * - * @param value - * @return - */ - public static HeaderTag tagFromString(String value) { - for (HeaderTag tagName : values()) { - if (tagName.toString().equals(value)) { - return tagName; - } + private HeaderTag(String value, String comment) { + this.fitsValCom = new FitsValCom(value, comment); } - return NOMATCH; - } - /** - * Return the "Source Data Product" (SDP) string to be used in a product file naming convention. - * The SDP string may not always be the same as the data source. - * - * For example, for ALTWG products, the dataSource could be "OLA" but the SDP string in the - * filename is supposed to be "alt". - * - * @param dataSource - * @return - */ - public static String getSDP(String dataSource) { - String sdp = dataSource; - if (dataSource.equals("ola")) { - sdp = "alt"; + public String value() { + return this.fitsValCom.getV(); + } + + public String comment() { + return this.fitsValCom.getC(); + } + + /** + * Contains all valid Fits keywords for this Enum. 'NOMATCH' is not a valid Fits keyword + */ + public static final EnumSet fitsKeywords = EnumSet.range(HeaderTag.SIMPLE, HeaderTag.GSDINDXI); + + public static final EnumSet globalDTMFitsData = EnumSet.of(HeaderTag.CLAT, HeaderTag.CLON); + + /** + * Return the HeaderTag associated with a given string. returns NOMATCH enum if no match found. + * + * @param value + * @return + */ + public static HeaderTag tagFromString(String value) { + for (HeaderTag tagName : values()) { + if (tagName.toString().equals(value)) { + return tagName; + } + } + return NOMATCH; + } + + /** + * Return the "Source Data Product" (SDP) string to be used in a product file naming convention. + * The SDP string may not always be the same as the data source. + * + * For example, for ALTWG products, the dataSource could be "OLA" but the SDP string in the + * filename is supposed to be "alt". + * + * @param dataSource + * @return + */ + public static String getSDP(String dataSource) { + String sdp = dataSource; + if (dataSource.equals("ola")) { + sdp = "alt"; + } + return sdp; } - return sdp; - } } diff --git a/src/main/java/terrasaur/fits/NFTmln.java b/src/main/java/terrasaur/fits/NFTmln.java index c6010e8..f90a212 100644 --- a/src/main/java/terrasaur/fits/NFTmln.java +++ b/src/main/java/terrasaur/fits/NFTmln.java @@ -35,157 +35,150 @@ import terrasaur.utils.DTMHeader; * HeaderCardFactory static methods. This is because the NFT MLN header updates are independent of * updates to the ALTWG product fits headers or the Map Formats headers. static methods in the * HeaderCardFactory - * + * * @author espirrc1 * */ public class NFTmln extends DTMFits implements DTMHeader { - // private FitsData fitsData; - private boolean dataContained = false; + // private FitsData fitsData; + private boolean dataContained = false; - public NFTmln(FitsHdr fitsHeader) { + public NFTmln(FitsHdr fitsHeader) { - super(fitsHeader, FitsHeaderType.NFTMLN); - } - - public List createFitsHeader(List planeList) throws HeaderCardException { - - List headerCards = new ArrayList(); - - headerCards.addAll(getHeaderInfo("Header Information")); - headerCards.addAll(getMissionInfo("Mission Information")); - headerCards.addAll(getIDInfo("Observation Information")); - // headerCards.addAll(getShapeSrcInfo()); - headerCards.addAll(getMapDataSrc("Data Source")); - headerCards.addAll(getTimingInfo()); - headerCards.addAll(getProcInfo("Processing Information")); - headerCards.addAll(getSpecificInfo("Product Specific Information")); - headerCards.addAll(getPlaneInfo("", planeList)); - - // end keyword - headerCards.add(getEnd()); - - return headerCards; - } - - /** - * Custom header block. - * - * @return - * @throws HeaderCardException - */ - @Override - public List getHeaderInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.NPRDVERS)); - - return headers; - - } - - /** - * Fits header block containing information about the source data used for the shape model. This - * method does not exist in the parent class. - * - * @return - * @throws HeaderCardException - */ - public List getShapeSrcInfo() throws HeaderCardException { - - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "Shape Data Source", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); - - return headers; - - } - - /** - * Custom source map data block. - */ - @Override - public List getMapDataSrc(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); - } - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); - - return headers; - - } - - /** - * Custom product specific info data block - * - * @throws HeaderCardException - */ - public List getSpecificInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + super(fitsHeader, FitsHeaderType.NFTMLN); } - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); - headers.addAll(getCornerCards()); - headers.addAll(getCenterVec()); - headers.addAll(getUX()); - headers.addAll(getUY()); - headers.addAll(getUZ()); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); - headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); + public List createFitsHeader(List planeList) throws HeaderCardException { - return headers; + List headerCards = new ArrayList(); - } + headerCards.addAll(getHeaderInfo("Header Information")); + headerCards.addAll(getMissionInfo("Mission Information")); + headerCards.addAll(getIDInfo("Observation Information")); + // headerCards.addAll(getShapeSrcInfo()); + headerCards.addAll(getMapDataSrc("Data Source")); + headerCards.addAll(getTimingInfo()); + headerCards.addAll(getProcInfo("Processing Information")); + headerCards.addAll(getSpecificInfo("Product Specific Information")); + headerCards.addAll(getPlaneInfo("", planeList)); - /** - * Fits header block containing information about when products were created. This method does not - * exist in the parent class. - * - * @return - * @throws HeaderCardException - */ - public List getTimingInfo() throws HeaderCardException { + // end keyword + headerCards.add(getEnd()); - List headers = new ArrayList(); - headers.add(new HeaderCard(COMMENT, "Timing Information", false)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.DATENPRD)); - - return headers; - - } - - /** - * Custom processing info block. - */ - public List getProcInfo(String comment) throws HeaderCardException { - - List headers = new ArrayList(); - if (comment.length() > 0) { - headers.add(new HeaderCard(COMMENT, comment, false)); + return headerCards; } - headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODVERS)); - headers.add(fitsHdr.getHeaderCard(HeaderTag.CREATOR)); - return headers; + /** + * Custom header block. + * + * @return + * @throws HeaderCardException + */ + @Override + public List getHeaderInfo(String comment) throws HeaderCardException { - } + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.NPRDVERS)); + return headers; + } + + /** + * Fits header block containing information about the source data used for the shape model. This + * method does not exist in the parent class. + * + * @return + * @throws HeaderCardException + */ + public List getShapeSrcInfo() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "Shape Data Source", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCF)); + + return headers; + } + + /** + * Custom source map data block. + */ + @Override + public List getMapDataSrc(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRC)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCV)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFTWARE)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SOFT_VER)); + + return headers; + } + + /** + * Custom product specific info data block + * + * @throws HeaderCardException + */ + public List getSpecificInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLON)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.CLAT)); + headers.addAll(getCornerCards()); + headers.addAll(getCenterVec()); + headers.addAll(getUX()); + headers.addAll(getUY()); + headers.addAll(getUZ()); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.GSD)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.SIGMA)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.SIG_DEF)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_1)); + headers.add(fitsHdr.getHeaderCardD(HeaderTag.DQUAL_2)); + + return headers; + } + + /** + * Fits header block containing information about when products were created. This method does not + * exist in the parent class. + * + * @return + * @throws HeaderCardException + */ + public List getTimingInfo() throws HeaderCardException { + + List headers = new ArrayList(); + headers.add(new HeaderCard(COMMENT, "Timing Information", false)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATASRCD)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.DATENPRD)); + + return headers; + } + + /** + * Custom processing info block. + */ + public List getProcInfo(String comment) throws HeaderCardException { + + List headers = new ArrayList(); + if (comment.length() > 0) { + headers.add(new HeaderCard(COMMENT, comment, false)); + } + headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODNAME)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.PRODVERS)); + headers.add(fitsHdr.getHeaderCard(HeaderTag.CREATOR)); + + return headers; + } } diff --git a/src/main/java/terrasaur/fits/ProductFits.java b/src/main/java/terrasaur/fits/ProductFits.java index c0339c5..f02940a 100644 --- a/src/main/java/terrasaur/fits/ProductFits.java +++ b/src/main/java/terrasaur/fits/ProductFits.java @@ -34,12 +34,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; -import org.apache.commons.io.FilenameUtils; import nom.tam.fits.BasicHDU; import nom.tam.fits.Fits; import nom.tam.fits.FitsException; import nom.tam.fits.HeaderCard; import nom.tam.util.Cursor; +import org.apache.commons.io.FilenameUtils; import terrasaur.enums.FitsHeaderType; import terrasaur.enums.PlaneInfo; import terrasaur.fits.FitsHdr.FitsHdrBuilder; @@ -53,371 +53,360 @@ import terrasaur.utils.xml.AsciiFile; */ public class ProductFits { - public static final String PLANE = "PLANE"; - public static final String COMMENT = "COMMENT"; + public static final String PLANE = "PLANE"; + public static final String COMMENT = "COMMENT"; - /** - * Open the fits file and extract fits keywords which start with "PLANE". The convention is that - * these are the keywords which describe the planes in the fits datacube. - * - * @param fitsFile - * @return - * @throws FitsException - * @throws IOException - */ - public static List getPlaneHeaderCards(String fitsFile) - throws FitsException, IOException { - Fits inFits = new Fits(fitsFile); - List planeHeaders = getPlaneHeaderCards(inFits); - return planeHeaders; - } - - /** - * Open the fits object and extract fits keywords which start with "PLANE". The convention is that - * these are the keywords which describe the planes in the fits datacube. - * - * @param inFitsFile - * @return - * @throws FitsException - * @throws IOException - */ - public static List getPlaneHeaderCards(Fits inFitsFile) - throws FitsException, IOException { - BasicHDU inHdu = inFitsFile.getHDU(0); - List planeHeaders = getPlaneHeaderCards(inHdu); - return planeHeaders; - } - - /** - * Parse the HeaderDataUnit (HDU) and extract fits keywords which start with "PLANE". The - * convention is that these are the keywords which describe the planes in the fits datacube. - * - * @param inHdu - * @return - */ - public static List getPlaneHeaderCards(BasicHDU inHdu) { - List planeHeaders = new ArrayList(); - Cursor cursor = inHdu.getHeader().iterator(); - while (cursor.hasNext()) { - HeaderCard hc = (HeaderCard) cursor.next(); - if (hc.getKey().startsWith(PLANE)) planeHeaders.add(hc); - } - return planeHeaders; - } - - /** - * Parse fits header and determine min/max latitude and longitude. For global fits files will just - * parse keywords that directly contain the min/max lat/lon values. For regional fits files will - * parse the latlon corner keywords and determine min, max lat/lon values. - * - * @param fitsFile - * @return Map<String, Double> where string is the .toString() of HeaderTags MINLON, MAXLON, - * MINLAT, MAXLAT. - * @throws IOException - * @throws FitsException - */ - public static Map minMaxLLFromFits(File fitsFile) - throws FitsException, IOException { - - System.out.println("Determining minmax lat lon from fits file:" + fitsFile.getAbsolutePath()); - - // initialize output - Map minMaxLL = new HashMap(); - - Map fitsHeaders = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); - - // check whether MINLON keyword exists. - String keyword = HeaderTag.MINLON.toString(); - boolean failOnNull = false; - HeaderCard thisCard = FitsUtil.getCard(fitsHeaders, keyword, failOnNull); - if (thisCard == null) { - - // assume LatLon corner keywords exist - // Map cornerCards = latLonCornersFromMap(fitsHeaders); - - // iterate through Lat corners and find min/max values - double minLat = 999D; - double maxLat = -999D; - failOnNull = true; - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LLCLAT.toString(), failOnNull); - double thisValue = thisCard.getValue(Double.class, Double.NaN); - minLat = Math.min(minLat, thisValue); - maxLat = Math.max(maxLat, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.ULCLAT.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLat = Math.min(minLat, thisValue); - maxLat = Math.max(maxLat, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.URCLAT.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLat = Math.min(minLat, thisValue); - maxLat = Math.max(maxLat, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LRCLAT.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLat = Math.min(minLat, thisValue); - maxLat = Math.max(maxLat, thisValue); - - // double check center lat. For polar observations the center latitude could be - // either the minimum or maximum latitude - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.CLAT.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLat = Math.min(minLat, thisValue); - maxLat = Math.max(maxLat, thisValue); - - minMaxLL.put(HeaderTag.MINLAT.toString(), minLat); - minMaxLL.put(HeaderTag.MAXLAT.toString(), maxLat); - - // iterate through Lon corners and find min/max values - double minLon = 999D; - double maxLon = -999D; - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LLCLNG.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLon = Math.min(minLon, thisValue); - maxLon = Math.max(maxLon, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.ULCLNG.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLon = Math.min(minLon, thisValue); - maxLon = Math.max(maxLon, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.URCLNG.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLon = Math.min(minLon, thisValue); - maxLon = Math.max(maxLon, thisValue); - - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LRCLNG.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLon = Math.min(minLon, thisValue); - maxLon = Math.max(maxLon, thisValue); - - // double check center lon - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.CLON.toString(), failOnNull); - thisValue = thisCard.getValue(Double.class, Double.NaN); - minLon = Math.min(minLon, thisValue); - maxLon = Math.max(maxLon, thisValue); - - minMaxLL.put(HeaderTag.MINLON.toString(), minLon); - minMaxLL.put(HeaderTag.MAXLON.toString(), maxLon); - - } else { - - // assume min/max lat/lon keywords exist - - // already queried for MINLON - double thisVal = thisCard.getValue(Double.class, Double.NaN); - minMaxLL.put(HeaderTag.MINLON.toString(), thisVal); - - // get MAXLON - failOnNull = true; - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MAXLON.toString(), failOnNull); - thisVal = thisCard.getValue(Double.class, Double.NaN); - minMaxLL.put(HeaderTag.MAXLON.toString(), thisVal); - - // get MINLAT - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MINLAT.toString(), failOnNull); - thisVal = thisCard.getValue(Double.class, Double.NaN); - minMaxLL.put(HeaderTag.MINLAT.toString(), thisVal); - - // get MAXLAT - thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MAXLAT.toString(), failOnNull); - thisVal = thisCard.getValue(Double.class, Double.NaN); - minMaxLL.put(HeaderTag.MAXLAT.toString(), thisVal); - } - - return minMaxLL; - } - - /** - * Parse and extract list of PlaneInfo enumerations that describe the planes contained in the fits - * file. Return empty list if none of the planes match. - * - * @param fitsFile - * @return - * @throws IOException - * @throws FitsException - */ - public static List planesFromFits(String fitsFile) throws FitsException, IOException { - - List planes = new ArrayList(); - - // extract header cards - List planeCards = getPlaneHeaderCards(fitsFile); - - /* - * planeCards are assumed to follow the fits keyword naming convention of PLANEN, where N goes - * from 0 to the number of planes in the fits file - 1. They correspond directly to the order in - * which the data is stored in the data cube. For example, PLANE0 = "Latitude" indicates that - * the 0th plane in the fits datacube contains the latitude values. + /** + * Open the fits file and extract fits keywords which start with "PLANE". The convention is that + * these are the keywords which describe the planes in the fits datacube. + * + * @param fitsFile + * @return + * @throws FitsException + * @throws IOException */ - for (HeaderCard thisPlane : planeCards) { - PlaneInfo thisPlaneInfo = PlaneInfo.keyVal2Plane(thisPlane.getValue()); - if (thisPlaneInfo != null) { - planes.add(thisPlaneInfo); - } - } - return planes; - } - - /** - * Save data to a Fits Datacube. Assumes hdrBuilder is pre-loaded with all the keyword values that - * can be known prior to this method. An example of a keyword value that cannot be known prior to - * this method is the ALTWG filename. This can only be created within this method by using - * metadata contained in the FitsHdrBuilder. The map information in fitsData will also be used to - * populate hdrBuilder if it is available. If it is not available then the method assumes the - * information is already in hdrBuilder. Ex. if fitsData was populated by reading a fits file then - * it may not have vector to center or unit vectors describing the reference frame. However, that - * information may be in the fits header, which should be captured by hdrBuilder. - * - * @param fitsData - * @param planeList - * @param outFname - * @param hdrBuilder - * @param hdrType - * @param crossrefFile - if not null then method will create a cross-reference file. The - * cross-reference file allows the pipeline to cross reference an original filenaming - * convention with the mission specific one. - * @throws FitsException - * @throws IOException - */ - public static void saveDataCubeFits( - FitsData fitsData, - List planeList, - String outFname, - FitsHdrBuilder hdrBuilder, - FitsHeaderType hdrType, - File crossrefFile) - throws FitsException, IOException { - - hdrBuilder.setByFitsData(fitsData); - - String fitsFname = outFname; - - if (crossrefFile != null) { - // save outfile name in cross-reference file, for future reference - String path = FilenameUtils.getFullPath(outFname); - if (path.length() == 0) path = "."; - String outBaseName = - String.format("%s%s%s", path, File.pathSeparator, FilenameUtils.getBaseName(outFname)); - AsciiFile crfFile = new AsciiFile(crossrefFile.getAbsolutePath()); - crfFile.streamSToFile(outBaseName, 0); - crfFile.closeFile(); + public static List getPlaneHeaderCards(String fitsFile) throws FitsException, IOException { + Fits inFits = new Fits(fitsFile); + List planeHeaders = getPlaneHeaderCards(inFits); + return planeHeaders; } - // set date this product was produced - hdrBuilder.setDateprod(); - - DTMHeader fitsHeader = FitsHeaderFactory.getDTMHeader(hdrBuilder.build(), hdrType); - fitsHeader.setData(fitsData); - List headers = fitsHeader.createFitsHeader(PlaneInfo.planesToHeaderCard(planeList)); - - System.out.println("saving to fits file:" + fitsFname); - FitsUtil.saveFits(fitsData.getData(), fitsFname, headers); - } - - /** - * General method for saving 3D double array with fits header as defined in fitsHdrbuilder. - * Assumes FitsHdrBuilder contains all the keywords that will be written to the fits header in the - * order that they should be written. - * - * @param dataCube - * @param hdrBuilder - * @throws IOException - * @throws FitsException - */ - public static void saveDataCubeFits( - double[][][] dataCube, - FitsHdrBuilder hdrBuilder, - List planeHeaders, - String outFile) - throws FitsException, IOException { - - FitsHdr fitsHdr = hdrBuilder.build(); - List headers = new ArrayList(); - - for (String keyword : fitsHdr.fitsKV.keySet()) { - headers.add(fitsHdr.fitsKV.get(keyword)); + /** + * Open the fits object and extract fits keywords which start with "PLANE". The convention is that + * these are the keywords which describe the planes in the fits datacube. + * + * @param inFitsFile + * @return + * @throws FitsException + * @throws IOException + */ + public static List getPlaneHeaderCards(Fits inFitsFile) throws FitsException, IOException { + BasicHDU inHdu = inFitsFile.getHDU(0); + List planeHeaders = getPlaneHeaderCards(inHdu); + return planeHeaders; } - saveDataCubeFits(dataCube, headers, planeHeaders, outFile); - } - - /** - * General method for writing to a fits file a 3D double array with fits headers as defined in - * List and planeKeywords as defined in separate List. plane keywords - * defining the planes of the dataCube in a separate List - * - * @param dataCube - * @param headers - * @param planeKeywords - can be null. If so then assumes headers contains all the keyword - * information to be written to the file. - * @param outFname - * @throws IOException - * @throws FitsException - */ - public static void saveDataCubeFits( - double[][][] dataCube, - List headers, - List planeKeywords, - String outFname) - throws FitsException, IOException { - - // append planeHeaders - if (!planeKeywords.isEmpty()) { - headers.addAll(planeKeywords); + /** + * Parse the HeaderDataUnit (HDU) and extract fits keywords which start with "PLANE". The + * convention is that these are the keywords which describe the planes in the fits datacube. + * + * @param inHdu + * @return + */ + public static List getPlaneHeaderCards(BasicHDU inHdu) { + List planeHeaders = new ArrayList(); + Cursor cursor = inHdu.getHeader().iterator(); + while (cursor.hasNext()) { + HeaderCard hc = (HeaderCard) cursor.next(); + if (hc.getKey().startsWith(PLANE)) planeHeaders.add(hc); + } + return planeHeaders; } - FitsUtil.saveFits(dataCube, outFname, headers); - } - /** - * Save NFT MLN. - * - * @param fitsData - * @param planeList - * @param outfile - * @param hdrBuilder - * @param hdrType - * @param crossrefFile - * @throws FitsException - * @throws IOException - */ - public static void saveNftFits( - FitsData fitsData, - List planeList, - String outfile, - FitsHdrBuilder hdrBuilder, - FitsHeaderType hdrType, - File crossrefFile) - throws FitsException, IOException { + /** + * Parse fits header and determine min/max latitude and longitude. For global fits files will just + * parse keywords that directly contain the min/max lat/lon values. For regional fits files will + * parse the latlon corner keywords and determine min, max lat/lon values. + * + * @param fitsFile + * @return Map<String, Double> where string is the .toString() of HeaderTags MINLON, MAXLON, + * MINLAT, MAXLAT. + * @throws IOException + * @throws FitsException + */ + public static Map minMaxLLFromFits(File fitsFile) throws FitsException, IOException { - // public static void saveDataCubeFits(FitsData fitsData, List planeList, String - // outFname, - // FitsHdrBuilder hdrBuilder, FitsHeaderType hdrType, - // File crossrefFile) throws FitsException, IOException { + System.out.println("Determining minmax lat lon from fits file:" + fitsFile.getAbsolutePath()); - String outNftFile = outfile; - Path outPath = Paths.get(outfile); - String nftFitsName = outPath.getFileName().toString(); + // initialize output + Map minMaxLL = new HashMap(); - // need to replace the product name in the headers list. No need to change comments. - hdrBuilder.setVbyHeaderTag(HeaderTag.PRODNAME, nftFitsName); + Map fitsHeaders = FitsUtil.getFitsHeaderAsMap(fitsFile.getAbsolutePath()); - // set date this product was produced. Uses NFT specific keyword - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - Date date = new Date(); - hdrBuilder.setVbyHeaderTag(HeaderTag.DATENPRD, dateFormat.format(date)); + // check whether MINLON keyword exists. + String keyword = HeaderTag.MINLON.toString(); + boolean failOnNull = false; + HeaderCard thisCard = FitsUtil.getCard(fitsHeaders, keyword, failOnNull); + if (thisCard == null) { - FitsHdr fitsHeader = hdrBuilder.build(); + // assume LatLon corner keywords exist + // Map cornerCards = latLonCornersFromMap(fitsHeaders); - DTMHeader nftFitsHeader = FitsHeaderFactory.getDTMHeader(fitsHeader, FitsHeaderType.NFTMLN); - nftFitsHeader.setData(fitsData); + // iterate through Lat corners and find min/max values + double minLat = 999D; + double maxLat = -999D; + failOnNull = true; + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LLCLAT.toString(), failOnNull); + double thisValue = thisCard.getValue(Double.class, Double.NaN); + minLat = Math.min(minLat, thisValue); + maxLat = Math.max(maxLat, thisValue); - List headers = - nftFitsHeader.createFitsHeader(PlaneInfo.planesToHeaderCard(planeList)); + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.ULCLAT.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLat = Math.min(minLat, thisValue); + maxLat = Math.max(maxLat, thisValue); - System.out.println("Saving to " + outNftFile); - FitsUtil.saveFits(fitsData.getData(), outNftFile, headers); - } + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.URCLAT.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLat = Math.min(minLat, thisValue); + maxLat = Math.max(maxLat, thisValue); + + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LRCLAT.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLat = Math.min(minLat, thisValue); + maxLat = Math.max(maxLat, thisValue); + + // double check center lat. For polar observations the center latitude could be + // either the minimum or maximum latitude + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.CLAT.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLat = Math.min(minLat, thisValue); + maxLat = Math.max(maxLat, thisValue); + + minMaxLL.put(HeaderTag.MINLAT.toString(), minLat); + minMaxLL.put(HeaderTag.MAXLAT.toString(), maxLat); + + // iterate through Lon corners and find min/max values + double minLon = 999D; + double maxLon = -999D; + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LLCLNG.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLon = Math.min(minLon, thisValue); + maxLon = Math.max(maxLon, thisValue); + + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.ULCLNG.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLon = Math.min(minLon, thisValue); + maxLon = Math.max(maxLon, thisValue); + + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.URCLNG.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLon = Math.min(minLon, thisValue); + maxLon = Math.max(maxLon, thisValue); + + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.LRCLNG.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLon = Math.min(minLon, thisValue); + maxLon = Math.max(maxLon, thisValue); + + // double check center lon + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.CLON.toString(), failOnNull); + thisValue = thisCard.getValue(Double.class, Double.NaN); + minLon = Math.min(minLon, thisValue); + maxLon = Math.max(maxLon, thisValue); + + minMaxLL.put(HeaderTag.MINLON.toString(), minLon); + minMaxLL.put(HeaderTag.MAXLON.toString(), maxLon); + + } else { + + // assume min/max lat/lon keywords exist + + // already queried for MINLON + double thisVal = thisCard.getValue(Double.class, Double.NaN); + minMaxLL.put(HeaderTag.MINLON.toString(), thisVal); + + // get MAXLON + failOnNull = true; + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MAXLON.toString(), failOnNull); + thisVal = thisCard.getValue(Double.class, Double.NaN); + minMaxLL.put(HeaderTag.MAXLON.toString(), thisVal); + + // get MINLAT + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MINLAT.toString(), failOnNull); + thisVal = thisCard.getValue(Double.class, Double.NaN); + minMaxLL.put(HeaderTag.MINLAT.toString(), thisVal); + + // get MAXLAT + thisCard = FitsUtil.getCard(fitsHeaders, HeaderTag.MAXLAT.toString(), failOnNull); + thisVal = thisCard.getValue(Double.class, Double.NaN); + minMaxLL.put(HeaderTag.MAXLAT.toString(), thisVal); + } + + return minMaxLL; + } + + /** + * Parse and extract list of PlaneInfo enumerations that describe the planes contained in the fits + * file. Return empty list if none of the planes match. + * + * @param fitsFile + * @return + * @throws IOException + * @throws FitsException + */ + public static List planesFromFits(String fitsFile) throws FitsException, IOException { + + List planes = new ArrayList(); + + // extract header cards + List planeCards = getPlaneHeaderCards(fitsFile); + + /* + * planeCards are assumed to follow the fits keyword naming convention of PLANEN, where N goes + * from 0 to the number of planes in the fits file - 1. They correspond directly to the order in + * which the data is stored in the data cube. For example, PLANE0 = "Latitude" indicates that + * the 0th plane in the fits datacube contains the latitude values. + */ + for (HeaderCard thisPlane : planeCards) { + PlaneInfo thisPlaneInfo = PlaneInfo.keyVal2Plane(thisPlane.getValue()); + if (thisPlaneInfo != null) { + planes.add(thisPlaneInfo); + } + } + return planes; + } + + /** + * Save data to a Fits Datacube. Assumes hdrBuilder is pre-loaded with all the keyword values that + * can be known prior to this method. An example of a keyword value that cannot be known prior to + * this method is the ALTWG filename. This can only be created within this method by using + * metadata contained in the FitsHdrBuilder. The map information in fitsData will also be used to + * populate hdrBuilder if it is available. If it is not available then the method assumes the + * information is already in hdrBuilder. Ex. if fitsData was populated by reading a fits file then + * it may not have vector to center or unit vectors describing the reference frame. However, that + * information may be in the fits header, which should be captured by hdrBuilder. + * + * @param fitsData + * @param planeList + * @param outFname + * @param hdrBuilder + * @param hdrType + * @param crossrefFile - if not null then method will create a cross-reference file. The + * cross-reference file allows the pipeline to cross reference an original filenaming + * convention with the mission specific one. + * @throws FitsException + * @throws IOException + */ + public static void saveDataCubeFits( + FitsData fitsData, + List planeList, + String outFname, + FitsHdrBuilder hdrBuilder, + FitsHeaderType hdrType, + File crossrefFile) + throws FitsException, IOException { + + hdrBuilder.setByFitsData(fitsData); + + String fitsFname = outFname; + + if (crossrefFile != null) { + // save outfile name in cross-reference file, for future reference + String path = FilenameUtils.getFullPath(outFname); + if (path.length() == 0) path = "."; + String outBaseName = String.format("%s%s%s", path, File.pathSeparator, FilenameUtils.getBaseName(outFname)); + AsciiFile crfFile = new AsciiFile(crossrefFile.getAbsolutePath()); + crfFile.streamSToFile(outBaseName, 0); + crfFile.closeFile(); + } + + // set date this product was produced + hdrBuilder.setDateprod(); + + DTMHeader fitsHeader = FitsHeaderFactory.getDTMHeader(hdrBuilder.build(), hdrType); + fitsHeader.setData(fitsData); + List headers = fitsHeader.createFitsHeader(PlaneInfo.planesToHeaderCard(planeList)); + + System.out.println("saving to fits file:" + fitsFname); + FitsUtil.saveFits(fitsData.getData(), fitsFname, headers); + } + + /** + * General method for saving 3D double array with fits header as defined in fitsHdrbuilder. + * Assumes FitsHdrBuilder contains all the keywords that will be written to the fits header in the + * order that they should be written. + * + * @param dataCube + * @param hdrBuilder + * @throws IOException + * @throws FitsException + */ + public static void saveDataCubeFits( + double[][][] dataCube, FitsHdrBuilder hdrBuilder, List planeHeaders, String outFile) + throws FitsException, IOException { + + FitsHdr fitsHdr = hdrBuilder.build(); + List headers = new ArrayList(); + + for (String keyword : fitsHdr.fitsKV.keySet()) { + headers.add(fitsHdr.fitsKV.get(keyword)); + } + + saveDataCubeFits(dataCube, headers, planeHeaders, outFile); + } + + /** + * General method for writing to a fits file a 3D double array with fits headers as defined in + * List and planeKeywords as defined in separate List. plane keywords + * defining the planes of the dataCube in a separate List + * + * @param dataCube + * @param headers + * @param planeKeywords - can be null. If so then assumes headers contains all the keyword + * information to be written to the file. + * @param outFname + * @throws IOException + * @throws FitsException + */ + public static void saveDataCubeFits( + double[][][] dataCube, List headers, List planeKeywords, String outFname) + throws FitsException, IOException { + + // append planeHeaders + if (!planeKeywords.isEmpty()) { + headers.addAll(planeKeywords); + } + FitsUtil.saveFits(dataCube, outFname, headers); + } + + /** + * Save NFT MLN. + * + * @param fitsData + * @param planeList + * @param outfile + * @param hdrBuilder + * @param hdrType + * @param crossrefFile + * @throws FitsException + * @throws IOException + */ + public static void saveNftFits( + FitsData fitsData, + List planeList, + String outfile, + FitsHdrBuilder hdrBuilder, + FitsHeaderType hdrType, + File crossrefFile) + throws FitsException, IOException { + + // public static void saveDataCubeFits(FitsData fitsData, List planeList, String + // outFname, + // FitsHdrBuilder hdrBuilder, FitsHeaderType hdrType, + // File crossrefFile) throws FitsException, IOException { + + String outNftFile = outfile; + Path outPath = Paths.get(outfile); + String nftFitsName = outPath.getFileName().toString(); + + // need to replace the product name in the headers list. No need to change comments. + hdrBuilder.setVbyHeaderTag(HeaderTag.PRODNAME, nftFitsName); + + // set date this product was produced. Uses NFT specific keyword + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date date = new Date(); + hdrBuilder.setVbyHeaderTag(HeaderTag.DATENPRD, dateFormat.format(date)); + + FitsHdr fitsHeader = hdrBuilder.build(); + + DTMHeader nftFitsHeader = FitsHeaderFactory.getDTMHeader(fitsHeader, FitsHeaderType.NFTMLN); + nftFitsHeader.setData(fitsData); + + List headers = nftFitsHeader.createFitsHeader(PlaneInfo.planesToHeaderCard(planeList)); + + System.out.println("Saving to " + outNftFile); + FitsUtil.saveFits(fitsData.getData(), outNftFile, headers); + } } diff --git a/src/main/java/terrasaur/fits/UnitDir.java b/src/main/java/terrasaur/fits/UnitDir.java index 9b3d721..e21a812 100644 --- a/src/main/java/terrasaur/fits/UnitDir.java +++ b/src/main/java/terrasaur/fits/UnitDir.java @@ -23,26 +23,25 @@ package terrasaur.fits; public enum UnitDir { + UX { + public int getAxis() { + return 1; + } + }, - UX { - public int getAxis() { - return 1; - } - }, + UY { - UY { + public int getAxis() { + return 2; + } + }, - public int getAxis() { - return 2; - } - }, + UZ { - UZ { + public int getAxis() { + return 3; + } + }; - public int getAxis() { - return 3; - } - }; - - public abstract int getAxis(); + public abstract int getAxis(); } diff --git a/src/main/java/terrasaur/gui/TranslateTimeController.java b/src/main/java/terrasaur/gui/TranslateTimeController.java index daca0a0..2aa6ae9 100644 --- a/src/main/java/terrasaur/gui/TranslateTimeController.java +++ b/src/main/java/terrasaur/gui/TranslateTimeController.java @@ -36,118 +36,108 @@ import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.stage.Stage; +import spice.basic.SpiceException; import terrasaur.apps.TranslateTime; import terrasaur.utils.AppVersion; -import spice.basic.SpiceException; public class TranslateTimeController implements Initializable { - private TranslateTime tt; + private TranslateTime tt; - public TranslateTimeController(Stage stage) { - this.tt = TranslateTimeFX.tt; - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - this.title.setText("Translate Time"); - this.version.setText(AppVersion.getVersionString()); - - // populate SCLK menu - NavigableSet sclkIDs = new TreeSet<>(TranslateTimeFX.sclkIDs); - this.sclkChoice.getItems().addAll(sclkIDs); - this.sclkChoice.getSelectionModel().selectedIndexProperty() - .addListener(new ChangeListener() { - - @Override - public void changed(ObservableValue observable, Number oldValue, - Number newValue) { - tt.setSCLKKernel(sclkChoice.getItems().get((Integer) newValue)); - - } - - }); - this.sclkChoice.getSelectionModel().select(0); - - try { - String zTime = ZonedDateTime.now(ZoneOffset.UTC).toString().strip(); - // strip off the final "Z" - this.tt.setUTC(zTime.substring(0, zTime.length() - 1)); - updateTime(); - } catch (SpiceException e) { - e.printStackTrace(); + public TranslateTimeController(Stage stage) { + this.tt = TranslateTimeFX.tt; } - } - @FXML - private Label title; + @Override + public void initialize(URL location, ResourceBundle resources) { + this.title.setText("Translate Time"); + this.version.setText(AppVersion.getVersionString()); - @FXML - private Label version; + // populate SCLK menu + NavigableSet sclkIDs = new TreeSet<>(TranslateTimeFX.sclkIDs); + this.sclkChoice.getItems().addAll(sclkIDs); + this.sclkChoice.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { - @FXML - private TextField julianString; + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + tt.setSCLKKernel(sclkChoice.getItems().get((Integer) newValue)); + } + }); + this.sclkChoice.getSelectionModel().select(0); - @FXML - private void setJulian() throws NumberFormatException, SpiceException { - if (julianString.getText().trim().length() > 0) - tt.setJulianDate(Double.parseDouble(julianString.getText())); - updateTime(); - } + try { + String zTime = ZonedDateTime.now(ZoneOffset.UTC).toString().strip(); + // strip off the final "Z" + this.tt.setUTC(zTime.substring(0, zTime.length() - 1)); + updateTime(); + } catch (SpiceException e) { + e.printStackTrace(); + } + } - @FXML - private ChoiceBox sclkChoice; + @FXML + private Label title; - @FXML - private TextField sclkString; + @FXML + private Label version; - @FXML - private void setSCLK() throws SpiceException { - if (sclkString.getText().trim().length() > 0) - tt.setSCLK(sclkString.getText()); - updateTime(); - } + @FXML + private TextField julianString; - @FXML - private TextField tdbString; + @FXML + private void setJulian() throws NumberFormatException, SpiceException { + if (julianString.getText().trim().length() > 0) tt.setJulianDate(Double.parseDouble(julianString.getText())); + updateTime(); + } - @FXML - private void setTDB() throws SpiceException { - if (tdbString.getText().trim().length() > 0) - tt.setTDB(Double.parseDouble(tdbString.getText())); - updateTime(); - } + @FXML + private ChoiceBox sclkChoice; - @FXML - private TextField tdbCalendarString; + @FXML + private TextField sclkString; - @FXML - private void setTDBCalendar() throws SpiceException { - if (tdbCalendarString.getText().trim().length() > 0) - tt.setTDBCalendarString(tdbCalendarString.getText()); - updateTime(); - } + @FXML + private void setSCLK() throws SpiceException { + if (sclkString.getText().trim().length() > 0) tt.setSCLK(sclkString.getText()); + updateTime(); + } - @FXML - private TextField utcString; + @FXML + private TextField tdbString; - @FXML - private Label utcLabel; + @FXML + private void setTDB() throws SpiceException { + if (tdbString.getText().trim().length() > 0) tt.setTDB(Double.parseDouble(tdbString.getText())); + updateTime(); + } - @FXML - private void setUTC() throws SpiceException { - if (utcString.getText().trim().length() > 0) - tt.setUTC(utcString.getText()); - updateTime(); - } + @FXML + private TextField tdbCalendarString; - private void updateTime() throws SpiceException { - julianString.setText(tt.toJulian()); - sclkString.setText(tt.toSCLK().toString()); - tdbString.setText(String.format("%.6f", tt.toTDB().getTDBSeconds())); - tdbCalendarString.setText(tt.toTDB().toString("YYYY-MM-DDTHR:MN:SC.### ::TDB")); - utcString.setText(tt.toTDB().toUTCString("ISOC", 3)); - utcLabel.setText(String.format("UTC (DOY %s)", tt.toTDB().toString("DOY"))); - } + @FXML + private void setTDBCalendar() throws SpiceException { + if (tdbCalendarString.getText().trim().length() > 0) tt.setTDBCalendarString(tdbCalendarString.getText()); + updateTime(); + } + @FXML + private TextField utcString; + + @FXML + private Label utcLabel; + + @FXML + private void setUTC() throws SpiceException { + if (utcString.getText().trim().length() > 0) tt.setUTC(utcString.getText()); + updateTime(); + } + + private void updateTime() throws SpiceException { + julianString.setText(tt.toJulian()); + sclkString.setText(tt.toSCLK().toString()); + tdbString.setText(String.format("%.6f", tt.toTDB().getTDBSeconds())); + tdbCalendarString.setText(tt.toTDB().toString("YYYY-MM-DDTHR:MN:SC.### ::TDB")); + utcString.setText(tt.toTDB().toUTCString("ISOC", 3)); + utcLabel.setText(String.format("UTC (DOY %s)", tt.toTDB().toString("DOY"))); + } } diff --git a/src/main/java/terrasaur/gui/TranslateTimeFX.java b/src/main/java/terrasaur/gui/TranslateTimeFX.java index 66aa4fb..275f404 100644 --- a/src/main/java/terrasaur/gui/TranslateTimeFX.java +++ b/src/main/java/terrasaur/gui/TranslateTimeFX.java @@ -33,34 +33,33 @@ import terrasaur.apps.TranslateTime; public class TranslateTimeFX extends Application { - // package private so it's visible from TranslateTimeController - static TranslateTime tt; - static Collection sclkIDs; + // package private so it's visible from TranslateTimeController + static TranslateTime tt; + static Collection sclkIDs; - public static void setSCLKIDs(Collection sclkIDs) { - TranslateTimeFX.sclkIDs = sclkIDs; - } + public static void setSCLKIDs(Collection sclkIDs) { + TranslateTimeFX.sclkIDs = sclkIDs; + } - public static void setTranslateTime(TranslateTime tt) { - TranslateTimeFX.tt = tt; - } + public static void setTranslateTime(TranslateTime tt) { + TranslateTimeFX.tt = tt; + } - public static void main(String[] args) { - launch(args); - Platform.exit(); - } + public static void main(String[] args) { + launch(args); + Platform.exit(); + } - @Override - public void start(Stage stage) throws Exception { - FXMLLoader loader = new FXMLLoader(); - loader.setLocation(getClass().getResource("/terrasaur/gui/TranslateTime.fxml")); - TranslateTimeController controller = new TranslateTimeController(stage); - loader.setController(controller); - - Parent root = loader.load(); - Scene scene = new Scene(root); - stage.setScene(scene); - stage.show(); - } + @Override + public void start(Stage stage) throws Exception { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/terrasaur/gui/TranslateTime.fxml")); + TranslateTimeController controller = new TranslateTimeController(stage); + loader.setController(controller); + Parent root = loader.load(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } } diff --git a/src/main/java/terrasaur/smallBodyModel/BoundingBox.java b/src/main/java/terrasaur/smallBodyModel/BoundingBox.java index bb0f2c6..08021f8 100644 --- a/src/main/java/terrasaur/smallBodyModel/BoundingBox.java +++ b/src/main/java/terrasaur/smallBodyModel/BoundingBox.java @@ -22,7 +22,6 @@ */ package terrasaur.smallBodyModel; - import picante.math.intervals.Interval; import picante.math.intervals.UnwritableInterval; import picante.math.vectorspace.UnwritableVectorIJK; @@ -31,226 +30,227 @@ import picante.math.vectorspace.VectorIJK; /** * The BoundingBox class is a data structure for storing the bounding box enclosing the a * 3-dimensional box-shaped region. - * + * * @author kahneg1 * @version 1.0 * */ public class BoundingBox { - private Interval xRange; - private Interval yRange; - private Interval zRange; + private Interval xRange; + private Interval yRange; + private Interval zRange; - /** - * Return a BoundingBox with each range set to {@link Interval#ALL_DOUBLES}. - */ - public BoundingBox() { - this(Interval.ALL_DOUBLES, Interval.ALL_DOUBLES, Interval.ALL_DOUBLES); - } - - /** - * Return a bounding box with its X limits set to elements 0 and 1, Y limits set to elements 2 and - * 3, and Z limits set to elements 4 and 5 of the input array. - * - * @param bounds - */ - public BoundingBox(double[] bounds) { - this(new Interval(bounds[0], bounds[1]), new Interval(bounds[2], bounds[3]), - new Interval(bounds[4], bounds[5])); - } - - /** - * Return a BoundingBox with the supplied dimensions. - * - * @param xRange - * @param yRange - * @param zRange - */ - public BoundingBox(UnwritableInterval xRange, UnwritableInterval yRange, - UnwritableInterval zRange) { - this.xRange = new Interval(xRange); - this.yRange = new Interval(yRange); - this.zRange = new Interval(zRange); - } - - /** - * Return a new BoundingBox with the same center as this instance but with each side's length - * scaled by scale. - * - * @param scale - * @return - */ - public BoundingBox getScaledBoundingBox(double scale) { - double center = xRange.getMiddle(); - double length = xRange.getLength() * scale; - Interval newXRange = new Interval(center - length / 2, center + length / 2); - - center = yRange.getMiddle(); - length = yRange.getLength() * scale; - Interval newYRange = new Interval(center - length / 2, center + length / 2); - - center = zRange.getMiddle(); - length = zRange.getLength() * scale; - Interval newZRange = new Interval(center - length / 2, center + length / 2); - - return new BoundingBox(newXRange, newYRange, newZRange); - } - - /** Set the X dimension */ - public void setXRange(UnwritableInterval range) { - this.xRange.setTo(range); - } - - /** Set the Y dimension */ - public void setYRange(UnwritableInterval range) { - this.yRange.setTo(range); - } - - /** Set the Z dimension */ - public void setZRange(UnwritableInterval range) { - this.zRange.setTo(range); - } - - /** Return the X dimension */ - public UnwritableInterval getXRange() { - return new UnwritableInterval(xRange); - } - - /** Return the Y dimension */ - public UnwritableInterval getYRange() { - return new UnwritableInterval(yRange); - } - - /** Return the Z dimension */ - public UnwritableInterval getZRange() { - return new UnwritableInterval(zRange); - } - - /** - * Expand the bounding box to contain the supplied point if it is not already contained. - * - * @param point - */ - public void update(UnwritableVectorIJK point) { - xRange.set(Math.min(point.getI(), xRange.getBegin()), Math.max(point.getI(), xRange.getEnd())); - yRange.set(Math.min(point.getJ(), yRange.getBegin()), Math.max(point.getJ(), yRange.getEnd())); - zRange.set(Math.min(point.getK(), zRange.getBegin()), Math.max(point.getK(), zRange.getEnd())); - } - - /** - * Check if this instance intersects the other. The intersection test in each dimension is - * {@link UnwritableInterval#closedIntersects(UnwritableInterval)}. - * - * @param other - * @return - */ - public boolean intersects(BoundingBox other) { - return xRange.closedIntersects(other.xRange) && yRange.closedIntersects(other.yRange) - && zRange.closedIntersects(other.zRange); - } - - /** - * @return the length of the largest side of the box - */ - public double getLargestSide() { - return Math.max(xRange.getLength(), Math.max(yRange.getLength(), zRange.getLength())); - } - - /** - * @return the center of the box - */ - public VectorIJK getCenterPoint() { - return new VectorIJK(xRange.getMiddle(), yRange.getMiddle(), zRange.getMiddle()); - } - - /** - * @return the diagonal length of the box - */ - public double getDiagonalLength() { - VectorIJK vec = new VectorIJK(xRange.getLength(), yRange.getLength(), zRange.getLength()); - return vec.getLength(); - } - - /** - * Returns whether or not the given point is contained in the box. The contains test in each - * dimension is {@link UnwritableInterval#closedContains(double)}. - * - * @param pt - * @return - */ - public boolean contains(UnwritableVectorIJK pt) { - return xRange.closedContains(pt.getI()) && yRange.closedContains(pt.getJ()) - && zRange.closedContains(pt.getK()); - } - - /** - * Returns whether or not the given point is contained in the box. The contains test in each - * dimension is {@link UnwritableInterval#closedContains(double)}. - * - * @param pt - * @return - */ - public boolean contains(double[] pt) { - return contains(new VectorIJK(pt)); - } - - /** - * expand the X range by length on each side. - * - * @param length - */ - public void expandX(double length) { - xRange.set(xRange.getBegin() - length, xRange.getEnd() + length); - } - - /** - * expand the Y range by length on each side. - * - * @param length - */ - public void expandY(double length) { - yRange.set(yRange.getBegin() - length, yRange.getEnd() + length); - } - - /** - * expand the Z range by length on each side. - * - * @param length - */ - public void expandZ(double length) { - zRange.set(zRange.getBegin() - length, zRange.getEnd() + length); - } - - /** - * Increase the size of the bounding box by adding (subtracting) to each side a specified - * percentage of the bounding box diagonal - * - * @param fractionOfDiagonalLength must be positive - */ - public void increaseSize(double fractionOfDiagonalLength) { - if (fractionOfDiagonalLength > 0.0) { - double size = fractionOfDiagonalLength * getDiagonalLength(); - xRange.set(xRange.getBegin() - size, xRange.getEnd() + size); - yRange.set(yRange.getBegin() - size, yRange.getEnd() + size); - zRange.set(zRange.getBegin() - size, zRange.getEnd() + size); + /** + * Return a BoundingBox with each range set to {@link Interval#ALL_DOUBLES}. + */ + public BoundingBox() { + this(Interval.ALL_DOUBLES, Interval.ALL_DOUBLES, Interval.ALL_DOUBLES); } - } - @Override - public String toString() { - return "xmin: " + xRange.getBegin() + " xmax: " + xRange.getEnd() + " ymin: " - + yRange.getBegin() + " ymax: " + yRange.getEnd() + " zmin: " + zRange.getBegin() - + " zmax: " + zRange.getEnd(); - } + /** + * Return a bounding box with its X limits set to elements 0 and 1, Y limits set to elements 2 and + * 3, and Z limits set to elements 4 and 5 of the input array. + * + * @param bounds + */ + public BoundingBox(double[] bounds) { + this( + new Interval(bounds[0], bounds[1]), + new Interval(bounds[2], bounds[3]), + new Interval(bounds[4], bounds[5])); + } - @Override - public boolean equals(Object obj) { - BoundingBox b = (BoundingBox) obj; - return xRange.equals(b.xRange) && yRange.equals(b.yRange) && zRange.equals(b.zRange); - } + /** + * Return a BoundingBox with the supplied dimensions. + * + * @param xRange + * @param yRange + * @param zRange + */ + public BoundingBox(UnwritableInterval xRange, UnwritableInterval yRange, UnwritableInterval zRange) { + this.xRange = new Interval(xRange); + this.yRange = new Interval(yRange); + this.zRange = new Interval(zRange); + } - @Override - public Object clone() { - return new BoundingBox(xRange, yRange, zRange); - } + /** + * Return a new BoundingBox with the same center as this instance but with each side's length + * scaled by scale. + * + * @param scale + * @return + */ + public BoundingBox getScaledBoundingBox(double scale) { + double center = xRange.getMiddle(); + double length = xRange.getLength() * scale; + Interval newXRange = new Interval(center - length / 2, center + length / 2); + + center = yRange.getMiddle(); + length = yRange.getLength() * scale; + Interval newYRange = new Interval(center - length / 2, center + length / 2); + + center = zRange.getMiddle(); + length = zRange.getLength() * scale; + Interval newZRange = new Interval(center - length / 2, center + length / 2); + + return new BoundingBox(newXRange, newYRange, newZRange); + } + + /** Set the X dimension */ + public void setXRange(UnwritableInterval range) { + this.xRange.setTo(range); + } + + /** Set the Y dimension */ + public void setYRange(UnwritableInterval range) { + this.yRange.setTo(range); + } + + /** Set the Z dimension */ + public void setZRange(UnwritableInterval range) { + this.zRange.setTo(range); + } + + /** Return the X dimension */ + public UnwritableInterval getXRange() { + return new UnwritableInterval(xRange); + } + + /** Return the Y dimension */ + public UnwritableInterval getYRange() { + return new UnwritableInterval(yRange); + } + + /** Return the Z dimension */ + public UnwritableInterval getZRange() { + return new UnwritableInterval(zRange); + } + + /** + * Expand the bounding box to contain the supplied point if it is not already contained. + * + * @param point + */ + public void update(UnwritableVectorIJK point) { + xRange.set(Math.min(point.getI(), xRange.getBegin()), Math.max(point.getI(), xRange.getEnd())); + yRange.set(Math.min(point.getJ(), yRange.getBegin()), Math.max(point.getJ(), yRange.getEnd())); + zRange.set(Math.min(point.getK(), zRange.getBegin()), Math.max(point.getK(), zRange.getEnd())); + } + + /** + * Check if this instance intersects the other. The intersection test in each dimension is + * {@link UnwritableInterval#closedIntersects(UnwritableInterval)}. + * + * @param other + * @return + */ + public boolean intersects(BoundingBox other) { + return xRange.closedIntersects(other.xRange) + && yRange.closedIntersects(other.yRange) + && zRange.closedIntersects(other.zRange); + } + + /** + * @return the length of the largest side of the box + */ + public double getLargestSide() { + return Math.max(xRange.getLength(), Math.max(yRange.getLength(), zRange.getLength())); + } + + /** + * @return the center of the box + */ + public VectorIJK getCenterPoint() { + return new VectorIJK(xRange.getMiddle(), yRange.getMiddle(), zRange.getMiddle()); + } + + /** + * @return the diagonal length of the box + */ + public double getDiagonalLength() { + VectorIJK vec = new VectorIJK(xRange.getLength(), yRange.getLength(), zRange.getLength()); + return vec.getLength(); + } + + /** + * Returns whether or not the given point is contained in the box. The contains test in each + * dimension is {@link UnwritableInterval#closedContains(double)}. + * + * @param pt + * @return + */ + public boolean contains(UnwritableVectorIJK pt) { + return xRange.closedContains(pt.getI()) && yRange.closedContains(pt.getJ()) && zRange.closedContains(pt.getK()); + } + + /** + * Returns whether or not the given point is contained in the box. The contains test in each + * dimension is {@link UnwritableInterval#closedContains(double)}. + * + * @param pt + * @return + */ + public boolean contains(double[] pt) { + return contains(new VectorIJK(pt)); + } + + /** + * expand the X range by length on each side. + * + * @param length + */ + public void expandX(double length) { + xRange.set(xRange.getBegin() - length, xRange.getEnd() + length); + } + + /** + * expand the Y range by length on each side. + * + * @param length + */ + public void expandY(double length) { + yRange.set(yRange.getBegin() - length, yRange.getEnd() + length); + } + + /** + * expand the Z range by length on each side. + * + * @param length + */ + public void expandZ(double length) { + zRange.set(zRange.getBegin() - length, zRange.getEnd() + length); + } + + /** + * Increase the size of the bounding box by adding (subtracting) to each side a specified + * percentage of the bounding box diagonal + * + * @param fractionOfDiagonalLength must be positive + */ + public void increaseSize(double fractionOfDiagonalLength) { + if (fractionOfDiagonalLength > 0.0) { + double size = fractionOfDiagonalLength * getDiagonalLength(); + xRange.set(xRange.getBegin() - size, xRange.getEnd() + size); + yRange.set(yRange.getBegin() - size, yRange.getEnd() + size); + zRange.set(zRange.getBegin() - size, zRange.getEnd() + size); + } + } + + @Override + public String toString() { + return "xmin: " + xRange.getBegin() + " xmax: " + xRange.getEnd() + " ymin: " + + yRange.getBegin() + " ymax: " + yRange.getEnd() + " zmin: " + zRange.getBegin() + + " zmax: " + zRange.getEnd(); + } + + @Override + public boolean equals(Object obj) { + BoundingBox b = (BoundingBox) obj; + return xRange.equals(b.xRange) && yRange.equals(b.yRange) && zRange.equals(b.zRange); + } + + @Override + public Object clone() { + return new BoundingBox(xRange, yRange, zRange); + } } diff --git a/src/main/java/terrasaur/smallBodyModel/LocalModelCollection.java b/src/main/java/terrasaur/smallBodyModel/LocalModelCollection.java index 99e74bf..9796dc4 100644 --- a/src/main/java/terrasaur/smallBodyModel/LocalModelCollection.java +++ b/src/main/java/terrasaur/smallBodyModel/LocalModelCollection.java @@ -22,6 +22,7 @@ */ package terrasaur.smallBodyModel; +import com.google.common.collect.HashMultimap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -34,174 +35,174 @@ 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 com.google.common.collect.HashMultimap; import picante.math.vectorspace.VectorIJK; -import terrasaur.utils.math.MathConversions; import terrasaur.utils.PolyDataStatistics; import terrasaur.utils.PolyDataUtil; +import terrasaur.utils.math.MathConversions; import terrasaur.utils.tessellation.FibonacciSphere; import vtk.vtkPoints; import vtk.vtkPolyData; /** * Hold a collection of local shape models - * + * * @author Hari.Nair@jhuapl.edu * */ public class LocalModelCollection { - private final static Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - static class LocalModel { - final Vector3D center; - final String filename; + static class LocalModel { + final Vector3D center; + final String filename; - LocalModel(Vector3D center, String filename) { - this.center = center; - this.filename = filename; + LocalModel(Vector3D center, String filename) { + this.center = center; + this.filename = filename; + } } - } + // key is tile index, value is collection of localModels + private HashMultimap localModelMap; + private FibonacciSphere tessellation; + // key is filename, value is shape model + private ThreadLocal> localModels; - // key is tile index, value is collection of localModels - private HashMultimap localModelMap; - private FibonacciSphere tessellation; - // key is filename, value is shape model - private ThreadLocal> localModels; + private Double scale; + private Rotation rotation; - private Double scale; - private Rotation rotation; - - /** - * - * @param numTiles total number of tiles to use for sorting local models - */ - public LocalModelCollection(int numTiles, Double scale, Rotation rotation) { - localModelMap = HashMultimap.create(); - tessellation = new FibonacciSphere(numTiles); - localModels = new ThreadLocal<>(); - this.scale = scale; - this.rotation = rotation; - } - - /** - * Add a shape model. Models are stored in a map with the center as the key, so an entry with the - * same center as an existing entry will overwrite the existing one. - * - * @param latInRadians - * @param lonInRadians - * @param filename - */ - public void addModel(double latInRadians, double lonInRadians, String filename) { - Vector3D center = new Vector3D(lonInRadians, latInRadians); - long tileIndex = tessellation.getTileIndex(MathConversions.toVectorIJK(center)); - LocalModel lm = new LocalModel(center, filename); - localModelMap.put(tileIndex, lm); - } - - /** - * Return a shape model containing the supplied point. This may not be the only shape model that - * contains this point, just the first one found. - * - * @param point - * @return - */ - public SmallBodyModel get(Vector3D point) { - List filenames = getFilenames(point); - if (filenames.size() == 0) - logger.error("No shape models cover {}", point.toString()); - double[] origin = new double[3]; - double[] intersectPoint = new double[3]; - for (String filename : filenames) { - SmallBodyModel sbm = load(filename); - long intersect = - sbm.computeRayIntersection(origin, point.toArray(), 2 * point.getNorm(), intersectPoint); - - if (intersect != -1) - return sbm; + /** + * + * @param numTiles total number of tiles to use for sorting local models + */ + public LocalModelCollection(int numTiles, Double scale, Rotation rotation) { + localModelMap = HashMultimap.create(); + tessellation = new FibonacciSphere(numTiles); + localModels = new ThreadLocal<>(); + this.scale = scale; + this.rotation = rotation; } - logger.debug("Failed intersection for lon {}, lat {}", Math.toDegrees(point.getAlpha()), - Math.toDegrees(point.getDelta())); - return null; - } - /** - * Load a shape model after applying any rotation or scaling - * - * @param filename - * @return - */ - private SmallBodyModel load(String filename) { - Map map = localModels.get(); - if (map == null) { - map = new HashMap<>(); - localModels.set(map); + /** + * Add a shape model. Models are stored in a map with the center as the key, so an entry with the + * same center as an existing entry will overwrite the existing one. + * + * @param latInRadians + * @param lonInRadians + * @param filename + */ + public void addModel(double latInRadians, double lonInRadians, String filename) { + Vector3D center = new Vector3D(lonInRadians, latInRadians); + long tileIndex = tessellation.getTileIndex(MathConversions.toVectorIJK(center)); + LocalModel lm = new LocalModel(center, filename); + localModelMap.put(tileIndex, lm); } - SmallBodyModel sbm = map.get(filename); - if (sbm == null) { - logger.debug("Thread {}: Loading {}", Thread.currentThread().getId(), - FilenameUtils.getBaseName(filename)); - try { - vtkPolyData model = PolyDataUtil.loadShapeModel(filename); - if (scale != null || rotation != null) { - PolyDataStatistics stats = new PolyDataStatistics(model); - Vector3D center = new Vector3D(stats.getCentroid()); + /** + * Return a shape model containing the supplied point. This may not be the only shape model that + * contains this point, just the first one found. + * + * @param point + * @return + */ + public SmallBodyModel get(Vector3D point) { + List filenames = getFilenames(point); + if (filenames.size() == 0) logger.error("No shape models cover {}", point.toString()); + double[] origin = new double[3]; + double[] intersectPoint = new double[3]; + for (String filename : filenames) { + SmallBodyModel sbm = load(filename); + long intersect = sbm.computeRayIntersection(origin, point.toArray(), 2 * point.getNorm(), intersectPoint); - vtkPoints points = model.GetPoints(); - for (int i = 0; i < points.GetNumberOfPoints(); i++) { - Vector3D thisPoint = new Vector3D(points.GetPoint(i)); - if (scale != null) - thisPoint = thisPoint.subtract(center).scalarMultiply(scale).add(center); - if (rotation != null) - thisPoint = rotation.applyTo(thisPoint.subtract(center)).add(center); - points.SetPoint(i, thisPoint.toArray()); - } + if (intersect != -1) return sbm; + } + logger.debug( + "Failed intersection for lon {}, lat {}", + Math.toDegrees(point.getAlpha()), + Math.toDegrees(point.getDelta())); + return null; + } + + /** + * Load a shape model after applying any rotation or scaling + * + * @param filename + * @return + */ + private SmallBodyModel load(String filename) { + Map map = localModels.get(); + if (map == null) { + map = new HashMap<>(); + localModels.set(map); + } + SmallBodyModel sbm = map.get(filename); + if (sbm == null) { + + logger.debug("Thread {}: Loading {}", Thread.currentThread().getId(), FilenameUtils.getBaseName(filename)); + try { + vtkPolyData model = PolyDataUtil.loadShapeModel(filename); + if (scale != null || rotation != null) { + PolyDataStatistics stats = new PolyDataStatistics(model); + Vector3D center = new Vector3D(stats.getCentroid()); + + vtkPoints points = model.GetPoints(); + for (int i = 0; i < points.GetNumberOfPoints(); i++) { + Vector3D thisPoint = new Vector3D(points.GetPoint(i)); + if (scale != null) + thisPoint = thisPoint + .subtract(center) + .scalarMultiply(scale) + .add(center); + if (rotation != null) + thisPoint = + rotation.applyTo(thisPoint.subtract(center)).add(center); + points.SetPoint(i, thisPoint.toArray()); + } + } + + sbm = new SmallBodyModel(model); + } catch (Exception e) { + logger.error(e.getLocalizedMessage()); + } + + map.put(filename, sbm); + } + return map.get(filename); + } + + /** + * Return the local model with the closest center to point + * + * @param point + * @return null if no models have been loaded + */ + private List getFilenames(Vector3D point) { + VectorIJK ijk = MathConversions.toVectorIJK(point); + + // A sorted map of tiles by distance + NavigableMap distanceMap = tessellation.getDistanceMap(ijk); + + List smallBodyModels = new ArrayList<>(); + for (Double dist : distanceMap.keySet()) { + + // A set of local models with centers in this tile + Set localModelSet = localModelMap.get((long) distanceMap.get(dist)); + + if (localModelSet.size() > 0) { + NavigableMap localDistanceMap = new TreeMap<>(); + for (LocalModel localModel : localModelSet) { + double thisDist = Vector3D.angle(localModel.center, point); + localDistanceMap.put(thisDist, localModel); + } + // add all local models with centers within PI/4 of point + for (double localDist : + localDistanceMap.headMap(Math.PI / 4, true).keySet()) { + smallBodyModels.add(localDistanceMap.get(localDist).filename); + } + } } - sbm = new SmallBodyModel(model); - } catch (Exception e) { - logger.error(e.getLocalizedMessage()); - } - - map.put(filename, sbm); + return smallBodyModels; } - return map.get(filename); - } - - /** - * Return the local model with the closest center to point - * - * @param point - * @return null if no models have been loaded - */ - private List getFilenames(Vector3D point) { - VectorIJK ijk = MathConversions.toVectorIJK(point); - - // A sorted map of tiles by distance - NavigableMap distanceMap = tessellation.getDistanceMap(ijk); - - List smallBodyModels = new ArrayList<>(); - for (Double dist : distanceMap.keySet()) { - - // A set of local models with centers in this tile - Set localModelSet = localModelMap.get((long) distanceMap.get(dist)); - - if (localModelSet.size() > 0) { - NavigableMap localDistanceMap = new TreeMap<>(); - for (LocalModel localModel : localModelSet) { - double thisDist = Vector3D.angle(localModel.center, point); - localDistanceMap.put(thisDist, localModel); - } - // add all local models with centers within PI/4 of point - for (double localDist : localDistanceMap.headMap(Math.PI / 4, true).keySet()) { - smallBodyModels.add(localDistanceMap.get(localDist).filename); - } - } - } - - return smallBodyModels; - } - } diff --git a/src/main/java/terrasaur/smallBodyModel/SBMTStructure.java b/src/main/java/terrasaur/smallBodyModel/SBMTStructure.java index a5ea4a1..91ee5f3 100644 --- a/src/main/java/terrasaur/smallBodyModel/SBMTStructure.java +++ b/src/main/java/terrasaur/smallBodyModel/SBMTStructure.java @@ -28,124 +28,122 @@ import org.immutables.value.Value; import terrasaur.smallBodyModel.ImmutableSBMTStructure.Builder; /** - * + * *

-# SBMT Structure File
-# type,point
-# ------------------------------------------------------------------------------
-# File consists of a list of structures on each line.
-#
-# Each line is defined by 17 columns with the following:
-# <id> <name> <centerXYZ[3]> <centerLLR[3]> <coloringValue[4]> <diameter> <flattening> <regularAngle> <colorRGB> <label>*
-#
-#               id: Id of the structure
-#             name: Name of the structure
-#     centerXYZ[3]: 3 columns that define the structure center in 3D space
-#     centerLLR[3]: 3 columns that define the structure center in lat,lon,radius
-# coloringValue[4]: 4 columns that define the ellipse “standard” colorings. The
-#                   colorings are: slope (NA), elevation (NA), acceleration (NA), potential (NA)
-#         diameter: Diameter of (semimajor) axis of ellipse
-#       flattening: Flattening factor of ellipse. Range: [0.0, 1.0]
-#     regularAngle: Angle between the semimajor axis and the line of longitude
-#                   as projected onto the surface
-#         colorRGB: 1 column (of RGB values [0, 255] separated by commas with no
-#                   spaces). This column appears as a single textual column.
-#            label: Label of the structure
-#
-#
-# Please note the following:
-# - Each line is composed of columns separated by a tab character.
-# - Blank lines or lines that start with '#' are ignored.
-# - Angle units: degrees
-# - Length units: kilometers
+ * # SBMT Structure File
+ * # type,point
+ * # ------------------------------------------------------------------------------
+ * # File consists of a list of structures on each line.
+ * #
+ * # Each line is defined by 17 columns with the following:
+ * # <id> <name> <centerXYZ[3]> <centerLLR[3]> <coloringValue[4]> <diameter> <flattening> <regularAngle> <colorRGB> <label>*
+ * #
+ * #               id: Id of the structure
+ * #             name: Name of the structure
+ * #     centerXYZ[3]: 3 columns that define the structure center in 3D space
+ * #     centerLLR[3]: 3 columns that define the structure center in lat,lon,radius
+ * # coloringValue[4]: 4 columns that define the ellipse “standard” colorings. The
+ * #                   colorings are: slope (NA), elevation (NA), acceleration (NA), potential (NA)
+ * #         diameter: Diameter of (semimajor) axis of ellipse
+ * #       flattening: Flattening factor of ellipse. Range: [0.0, 1.0]
+ * #     regularAngle: Angle between the semimajor axis and the line of longitude
+ * #                   as projected onto the surface
+ * #         colorRGB: 1 column (of RGB values [0, 255] separated by commas with no
+ * #                   spaces). This column appears as a single textual column.
+ * #            label: Label of the structure
+ * #
+ * #
+ * # Please note the following:
+ * # - Each line is composed of columns separated by a tab character.
+ * # - Blank lines or lines that start with '#' are ignored.
+ * # - Angle units: degrees
+ * # - Length units: kilometers
  * 
- * + * * @author Hari.Nair@jhuapl.edu * */ @Value.Immutable public abstract class SBMTStructure { - abstract int id(); + abstract int id(); - abstract String name(); + abstract String name(); - public abstract Vector3D centerXYZ(); + public abstract Vector3D centerXYZ(); - abstract String slopeColoring(); + abstract String slopeColoring(); - abstract String elevationColoring(); + abstract String elevationColoring(); - abstract String accelerationColoring(); + abstract String accelerationColoring(); - abstract String potentialColoring(); + abstract String potentialColoring(); - abstract double diameter(); + abstract double diameter(); - abstract double flattening(); + abstract double flattening(); - abstract double regularAngle(); + abstract double regularAngle(); - abstract Color rgb(); + abstract Color rgb(); - abstract String label(); + abstract String label(); - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%d\t", id())); - sb.append(String.format("%s\t", name())); - sb.append(String.format("%.16f\t", centerXYZ().getX())); - sb.append(String.format("%.16f\t", centerXYZ().getY())); - sb.append(String.format("%.16f\t", centerXYZ().getZ())); - sb.append(String.format("%.16f\t", Math.toDegrees(centerXYZ().getDelta()))); - sb.append(String.format("%.16f\t", Math.toDegrees(centerXYZ().getAlpha()))); - sb.append(String.format("%.16f\t", centerXYZ().getNorm())); - sb.append(String.format("%s\t", slopeColoring())); - sb.append(String.format("%s\t", elevationColoring())); - sb.append(String.format("%s\t", accelerationColoring())); - sb.append(String.format("%s\t", potentialColoring())); - sb.append(String.format("%f\t", diameter())); - sb.append(String.format("%f\t", flattening())); - sb.append(String.format("%f\t", regularAngle())); - sb.append(String.format("%d,%d,%d\t", rgb().getRed(), rgb().getGreen(), rgb().getBlue())); - sb.append(label()); - return sb.toString(); - } - - public static SBMTStructure fromString(String line) { - String[] parts = line.split("\\s+"); - int id = Integer.parseInt(parts[0]); - String name = parts[1]; - Vector3D centerXYZ = new Vector3D(Double.parseDouble(parts[2]), Double.parseDouble(parts[3]), - Double.parseDouble(parts[4])); - String slopeColoring = parts[8]; - String elevationColoring = parts[9]; - String accelerationColoring = parts[10]; - String potentialColoring = parts[11]; - double diameter = Double.parseDouble(parts[12]); - double flattening = Double.parseDouble(parts[13]); - double regularAngle = Double.parseDouble(parts[14]); - String[] colorParts = parts[15].split(","); - Color rgb = new Color(Integer.parseInt(colorParts[0]), Integer.parseInt(colorParts[1]), - Integer.parseInt(colorParts[2])); - String label = parts[16]; - - Builder builder = ImmutableSBMTStructure.builder(); - builder.id(id); - builder.name(name); - builder.centerXYZ(centerXYZ); - builder.slopeColoring(slopeColoring); - builder.elevationColoring(elevationColoring); - builder.accelerationColoring(accelerationColoring); - builder.potentialColoring(potentialColoring); - builder.diameter(diameter); - builder.flattening(flattening); - builder.regularAngle(regularAngle); - builder.rgb(rgb); - builder.label(label); - return builder.build(); - } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%d\t", id())); + sb.append(String.format("%s\t", name())); + sb.append(String.format("%.16f\t", centerXYZ().getX())); + sb.append(String.format("%.16f\t", centerXYZ().getY())); + sb.append(String.format("%.16f\t", centerXYZ().getZ())); + sb.append(String.format("%.16f\t", Math.toDegrees(centerXYZ().getDelta()))); + sb.append(String.format("%.16f\t", Math.toDegrees(centerXYZ().getAlpha()))); + sb.append(String.format("%.16f\t", centerXYZ().getNorm())); + sb.append(String.format("%s\t", slopeColoring())); + sb.append(String.format("%s\t", elevationColoring())); + sb.append(String.format("%s\t", accelerationColoring())); + sb.append(String.format("%s\t", potentialColoring())); + sb.append(String.format("%f\t", diameter())); + sb.append(String.format("%f\t", flattening())); + sb.append(String.format("%f\t", regularAngle())); + sb.append(String.format("%d,%d,%d\t", rgb().getRed(), rgb().getGreen(), rgb().getBlue())); + sb.append(label()); + return sb.toString(); + } + public static SBMTStructure fromString(String line) { + String[] parts = line.split("\\s+"); + int id = Integer.parseInt(parts[0]); + String name = parts[1]; + Vector3D centerXYZ = + new Vector3D(Double.parseDouble(parts[2]), Double.parseDouble(parts[3]), Double.parseDouble(parts[4])); + String slopeColoring = parts[8]; + String elevationColoring = parts[9]; + String accelerationColoring = parts[10]; + String potentialColoring = parts[11]; + double diameter = Double.parseDouble(parts[12]); + double flattening = Double.parseDouble(parts[13]); + double regularAngle = Double.parseDouble(parts[14]); + String[] colorParts = parts[15].split(","); + Color rgb = new Color( + Integer.parseInt(colorParts[0]), Integer.parseInt(colorParts[1]), Integer.parseInt(colorParts[2])); + String label = parts[16]; + Builder builder = ImmutableSBMTStructure.builder(); + builder.id(id); + builder.name(name); + builder.centerXYZ(centerXYZ); + builder.slopeColoring(slopeColoring); + builder.elevationColoring(elevationColoring); + builder.accelerationColoring(accelerationColoring); + builder.potentialColoring(potentialColoring); + builder.diameter(diameter); + builder.flattening(flattening); + builder.regularAngle(regularAngle); + builder.rgb(rgb); + builder.label(label); + return builder.build(); + } } diff --git a/src/main/java/terrasaur/smallBodyModel/SmallBodyCubes.java b/src/main/java/terrasaur/smallBodyModel/SmallBodyCubes.java index a512f0f..4cbb615 100644 --- a/src/main/java/terrasaur/smallBodyModel/SmallBodyCubes.java +++ b/src/main/java/terrasaur/smallBodyModel/SmallBodyCubes.java @@ -32,175 +32,172 @@ import vtk.vtkPolyData; /** * This class is used to subdivide the bounding box of a shape model into a contiguous grid of 3D * cubes (sort of like voxels). - * + * * @author kahneg1 * @version 1.0 * */ public class SmallBodyCubes { - private final static Logger logger = LogManager.getLogger(SmallBodyCubes.class); + private static final Logger logger = LogManager.getLogger(SmallBodyCubes.class); - private BoundingBox boundingBox; - private ArrayList allCubes = new ArrayList(); - private final double cubeSize; - private final double buffer; + private BoundingBox boundingBox; + private ArrayList allCubes = new ArrayList(); + private final double cubeSize; + private final double buffer; - /** - * Create a cube set structure for the given model, where each cube has side cubeSize and - * buffer is added to all sides of the bounding box of the model. Cubes that do not - * intersect the asteroid at all are removed. - * - * @param smallBodyPolyData - * @param cubeSize - * @param buffer - */ - public SmallBodyCubes(vtkPolyData smallBodyPolyData, double cubeSize, double buffer, - boolean removeEmptyCubes) { - this.cubeSize = cubeSize; - this.buffer = buffer; + /** + * Create a cube set structure for the given model, where each cube has side cubeSize and + * buffer is added to all sides of the bounding box of the model. Cubes that do not + * intersect the asteroid at all are removed. + * + * @param smallBodyPolyData + * @param cubeSize + * @param buffer + */ + public SmallBodyCubes(vtkPolyData smallBodyPolyData, double cubeSize, double buffer, boolean removeEmptyCubes) { + this.cubeSize = cubeSize; + this.buffer = buffer; - initialize(smallBodyPolyData); + initialize(smallBodyPolyData); - if (removeEmptyCubes) - removeEmptyCubes(smallBodyPolyData); - } + if (removeEmptyCubes) removeEmptyCubes(smallBodyPolyData); + } - private void initialize(vtkPolyData smallBodyPolyData) { - smallBodyPolyData.ComputeBounds(); - double[] bounds = smallBodyPolyData.GetBounds(); - boundingBox = new BoundingBox(new Interval(bounds[0] - buffer, bounds[1] + buffer), - new Interval(bounds[2] - buffer, bounds[3] + buffer), - new Interval(bounds[4] - buffer, bounds[5] + buffer)); + private void initialize(vtkPolyData smallBodyPolyData) { + smallBodyPolyData.ComputeBounds(); + double[] bounds = smallBodyPolyData.GetBounds(); + boundingBox = new BoundingBox( + new Interval(bounds[0] - buffer, bounds[1] + buffer), + new Interval(bounds[2] - buffer, bounds[3] + buffer), + new Interval(bounds[4] - buffer, bounds[5] + buffer)); - int numCubesX = (int) Math.ceil(boundingBox.getXRange().getLength() / cubeSize); - int numCubesY = (int) Math.ceil(boundingBox.getYRange().getLength() / cubeSize); - int numCubesZ = (int) Math.ceil(boundingBox.getZRange().getLength() / cubeSize); + int numCubesX = (int) Math.ceil(boundingBox.getXRange().getLength() / cubeSize); + int numCubesY = (int) Math.ceil(boundingBox.getYRange().getLength() / cubeSize); + int numCubesZ = (int) Math.ceil(boundingBox.getZRange().getLength() / cubeSize); - for (int k = 0; k < numCubesZ; ++k) { - double zmin = boundingBox.getZRange().getBegin() + k * cubeSize; - double zmax = boundingBox.getZRange().getBegin() + (k + 1) * cubeSize; - for (int j = 0; j < numCubesY; ++j) { - double ymin = boundingBox.getYRange().getBegin() + j * cubeSize; - double ymax = boundingBox.getYRange().getBegin() + (j + 1) * cubeSize; - for (int i = 0; i < numCubesX; ++i) { - double xmin = boundingBox.getXRange().getBegin() + i * cubeSize; - double xmax = boundingBox.getXRange().getBegin() + (i + 1) * cubeSize; - BoundingBox bb = new BoundingBox(new Interval(xmin, xmax), new Interval(ymin, ymax), - new Interval(zmin, zmax)); - allCubes.add(bb); + for (int k = 0; k < numCubesZ; ++k) { + double zmin = boundingBox.getZRange().getBegin() + k * cubeSize; + double zmax = boundingBox.getZRange().getBegin() + (k + 1) * cubeSize; + for (int j = 0; j < numCubesY; ++j) { + double ymin = boundingBox.getYRange().getBegin() + j * cubeSize; + double ymax = boundingBox.getYRange().getBegin() + (j + 1) * cubeSize; + for (int i = 0; i < numCubesX; ++i) { + double xmin = boundingBox.getXRange().getBegin() + i * cubeSize; + double xmax = boundingBox.getXRange().getBegin() + (i + 1) * cubeSize; + BoundingBox bb = new BoundingBox( + new Interval(xmin, xmax), new Interval(ymin, ymax), new Interval(zmin, zmax)); + allCubes.add(bb); + } + } } - } - } - } - - private void removeEmptyCubes(vtkPolyData smallBodyPolyData) { - logger.info("total cubes before reduction = {}", allCubes.size()); - - // Remove from allCubes all cubes that do not intersect the asteroid - // long t0 = System.currentTimeMillis(); - TreeSet intersectingCubes = getIntersectingCubes(smallBodyPolyData); - // System.out.println("Time elapsed: " + - // ((double)System.currentTimeMillis()-t0)/1000.0); - - ArrayList tmpCubes = new ArrayList(); - for (Integer i : intersectingCubes) { - tmpCubes.add(allCubes.get(i)); } - allCubes = tmpCubes; + private void removeEmptyCubes(vtkPolyData smallBodyPolyData) { + logger.info("total cubes before reduction = {}", allCubes.size()); - logger.info("finished initializing cubes, total = {}", allCubes.size()); - } + // Remove from allCubes all cubes that do not intersect the asteroid + // long t0 = System.currentTimeMillis(); + TreeSet intersectingCubes = getIntersectingCubes(smallBodyPolyData); + // System.out.println("Time elapsed: " + + // ((double)System.currentTimeMillis()-t0)/1000.0); - public BoundingBox getCube(int cubeId) { - return allCubes.get(cubeId); - } + ArrayList tmpCubes = new ArrayList(); + for (Integer i : intersectingCubes) { + tmpCubes.add(allCubes.get(i)); + } - /** - * Get all the cubes that intersect with polydata - * - * @param polydata - * @return - */ - public TreeSet getIntersectingCubes(vtkPolyData polydata) { - TreeSet cubeIds = new TreeSet(); + allCubes = tmpCubes; - // Iterate through each cube and check if it intersects - // with the bounding box of any of the polygons of the polydata - BoundingBox polydataBB = new BoundingBox(polydata.GetBounds()); - - long numberPolygons = polydata.GetNumberOfCells(); - - // Store all the bounding boxes of all the individual polygons in an - // array first - // since the call to GetCellBounds is very slow. - double[] cellBounds = new double[6]; - ArrayList polyCellsBB = new ArrayList(); - for (int j = 0; j < numberPolygons; ++j) { - polydata.GetCellBounds(j, cellBounds); - polyCellsBB.add(new BoundingBox(cellBounds)); + logger.info("finished initializing cubes, total = {}", allCubes.size()); } - int numberCubes = allCubes.size(); - for (int i = 0; i < numberCubes; ++i) { - // Before checking each polygon individually, first see if the - // polydata as a whole intersects the cube - BoundingBox cube = getCube(i); - if (cube.intersects(polydataBB)) { + public BoundingBox getCube(int cubeId) { + return allCubes.get(cubeId); + } + + /** + * Get all the cubes that intersect with polydata + * + * @param polydata + * @return + */ + public TreeSet getIntersectingCubes(vtkPolyData polydata) { + TreeSet cubeIds = new TreeSet(); + + // Iterate through each cube and check if it intersects + // with the bounding box of any of the polygons of the polydata + BoundingBox polydataBB = new BoundingBox(polydata.GetBounds()); + + long numberPolygons = polydata.GetNumberOfCells(); + + // Store all the bounding boxes of all the individual polygons in an + // array first + // since the call to GetCellBounds is very slow. + double[] cellBounds = new double[6]; + ArrayList polyCellsBB = new ArrayList(); for (int j = 0; j < numberPolygons; ++j) { - BoundingBox bb = polyCellsBB.get(j); - if (cube.intersects(bb)) { - cubeIds.add(i); - break; - } + polydata.GetCellBounds(j, cellBounds); + polyCellsBB.add(new BoundingBox(cellBounds)); } - } + + int numberCubes = allCubes.size(); + for (int i = 0; i < numberCubes; ++i) { + // Before checking each polygon individually, first see if the + // polydata as a whole intersects the cube + BoundingBox cube = getCube(i); + if (cube.intersects(polydataBB)) { + for (int j = 0; j < numberPolygons; ++j) { + BoundingBox bb = polyCellsBB.get(j); + if (cube.intersects(bb)) { + cubeIds.add(i); + break; + } + } + } + } + + return cubeIds; } - return cubeIds; - } + /** + * Get all the cubes that intersect with BoundingBox bb + * + * @param bb + * @return + */ + public TreeSet getIntersectingCubes(BoundingBox bb) { + TreeSet cubeIds = new TreeSet(); - /** - * Get all the cubes that intersect with BoundingBox bb - * - * @param bb - * @return - */ - public TreeSet getIntersectingCubes(BoundingBox bb) { - TreeSet cubeIds = new TreeSet(); + int numberCubes = allCubes.size(); + for (int i = 0; i < numberCubes; ++i) { + BoundingBox cube = getCube(i); + if (cube.intersects(bb)) { + cubeIds.add(i); + } + } - int numberCubes = allCubes.size(); - for (int i = 0; i < numberCubes; ++i) { - BoundingBox cube = getCube(i); - if (cube.intersects(bb)) { - cubeIds.add(i); - } + return cubeIds; } - return cubeIds; - } + /** + * Get the id of the cube containing point + * + * @param point + * @return + */ + public int getCubeId(double[] point) { + if (!boundingBox.contains(point)) return -1; - /** - * Get the id of the cube containing point - * - * @param point - * @return - */ - public int getCubeId(double[] point) { - if (!boundingBox.contains(point)) - return -1; + int numberCubes = allCubes.size(); + for (int i = 0; i < numberCubes; ++i) { + BoundingBox cube = getCube(i); + if (cube.contains(point)) return i; + } - int numberCubes = allCubes.size(); - for (int i = 0; i < numberCubes; ++i) { - BoundingBox cube = getCube(i); - if (cube.contains(point)) - return i; + // If we reach here something is wrong + System.err.println("Error: could not find cube"); + + return -1; } - - // If we reach here something is wrong - System.err.println("Error: could not find cube"); - - return -1; - } } diff --git a/src/main/java/terrasaur/smallBodyModel/SmallBodyModel.java b/src/main/java/terrasaur/smallBodyModel/SmallBodyModel.java index b5b7075..158d6dd 100644 --- a/src/main/java/terrasaur/smallBodyModel/SmallBodyModel.java +++ b/src/main/java/terrasaur/smallBodyModel/SmallBodyModel.java @@ -28,7 +28,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.TreeSet; - import org.apache.commons.math3.geometry.euclidean.threed.Rotation; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; @@ -58,554 +57,541 @@ import vtk.vtksbModifiedBSPTree; * The SmallBodyModel class represents a shape model of a small body such as Bennu or Eros. It * contains methods for common operations on a shape model such as searching for closest points or * cells, cutting out an elliptical region or boundary, or exporting to different formats. - * + * * @author kahneg1 * @version 1.0 - * + * */ public class SmallBodyModel { - private final static Logger logger = LogManager.getLogger(SmallBodyModel.class); + private static final Logger logger = LogManager.getLogger(SmallBodyModel.class); - public enum ColoringValueType { - POINT_DATA, CELLDATA - } - - private vtkPolyData smallBodyPolyData; - private vtkPolyData lowResSmallBodyPolyData; - private vtksbCellLocator cellLocator; - private vtksbModifiedBSPTree bspLocator; - private vtkOctreePointLocator pointLocator; - private vtkOctreePointLocator lowResPointLocator; - private SmallBodyCubes smallBodyCubes; - private File defaultModelFile; - private int resolutionLevel = 0; - private vtkGenericCell genericCell; - private String[] modelNames; - private BoundingBox boundingBox = null; - - private vtkFloatArray cellNormals; - private vtkIdList idList; // to avoid repeated allocations - private vtkIdList idList2; // to avoid repeated allocations - - /** - * Default constructor. Must be followed by a call to setSmallBodyPolyData. - */ - public SmallBodyModel() { - smallBodyPolyData = new vtkPolyData(); - genericCell = new vtkGenericCell(); - idList = new vtkIdList(); - idList2 = new vtkIdList(); - } - - /** - * Convenience method for initializing a SmallBodyModel with just a vtkPolyData. - * - * @param polyData - */ - public SmallBodyModel(vtkPolyData polyData) { - this(); - - vtkFloatArray[] coloringValues = {}; - String[] coloringNames = {}; - String[] coloringUnits = {}; - ColoringValueType coloringValueType = ColoringValueType.CELLDATA; - - setSmallBodyPolyData(polyData, coloringValues, coloringNames, coloringUnits, coloringValueType); - } - - public void setSmallBodyPolyData(vtkPolyData polydata, vtkFloatArray[] coloringValues, - String[] coloringNames, String[] coloringUnits, ColoringValueType coloringValueType) { - smallBodyPolyData.DeepCopy(polydata); - - smallBodyPolyData.BuildLinks(0); - - initializeLocators(); - - lowResSmallBodyPolyData = smallBodyPolyData; - lowResPointLocator = pointLocator; - } - - public boolean isBuiltIn() { - return true; - } - - private void initializeLocators() { - if (cellLocator == null) { - cellLocator = new vtksbCellLocator(); - bspLocator = new vtksbModifiedBSPTree(); - pointLocator = new vtkOctreePointLocator(); + public enum ColoringValueType { + POINT_DATA, + CELLDATA } - // Initialize the cell locator - cellLocator.FreeSearchStructure(); - cellLocator.SetDataSet(smallBodyPolyData); - cellLocator.CacheCellBoundsOn(); - cellLocator.AutomaticOn(); - // cellLocator.SetMaxLevel(10); - // cellLocator.SetNumberOfCellsPerNode(5); - cellLocator.BuildLocator(); + private vtkPolyData smallBodyPolyData; + private vtkPolyData lowResSmallBodyPolyData; + private vtksbCellLocator cellLocator; + private vtksbModifiedBSPTree bspLocator; + private vtkOctreePointLocator pointLocator; + private vtkOctreePointLocator lowResPointLocator; + private SmallBodyCubes smallBodyCubes; + private File defaultModelFile; + private int resolutionLevel = 0; + private vtkGenericCell genericCell; + private String[] modelNames; + private BoundingBox boundingBox = null; - // Initialize the BSP locator - bspLocator.FreeSearchStructure(); - bspLocator.SetDataSet(smallBodyPolyData); - bspLocator.CacheCellBoundsOn(); - bspLocator.AutomaticOn(); - // bspLocator.SetMaxLevel(10); - // bspLocator.SetNumberOfCellsPerNode(5); - bspLocator.BuildLocator(); + private vtkFloatArray cellNormals; + private vtkIdList idList; // to avoid repeated allocations + private vtkIdList idList2; // to avoid repeated allocations - pointLocator.FreeSearchStructure(); - pointLocator.SetDataSet(smallBodyPolyData); - pointLocator.BuildLocator(); - } - - private void initializeLowResData() { - if (lowResPointLocator == null) { - lowResSmallBodyPolyData = new vtkPolyData(); - - try { - lowResSmallBodyPolyData.ShallowCopy( - PolyDataUtil.loadShapeModelAndComputeNormals(defaultModelFile.getAbsolutePath())); - } catch (Exception e) { - e.printStackTrace(); - } - - lowResPointLocator = new vtkOctreePointLocator(); - lowResPointLocator.SetDataSet(lowResSmallBodyPolyData); - lowResPointLocator.BuildLocator(); - } - } - - public vtkPolyData getSmallBodyPolyData() { - return smallBodyPolyData; - } - - public vtkPolyData getLowResSmallBodyPolyData() { - initializeLowResData(); - - return lowResSmallBodyPolyData; - } - - public vtksbCellLocator getCellLocator() { - return cellLocator; - } - - public vtkAbstractPointLocator getPointLocator() { - return pointLocator; - } - - public SmallBodyCubes getSmallBodyCubes() { - if (smallBodyCubes == null) { - // The number 38.66056033363347 is used here so that the cube size - // comes out to 1 km for Eros. - - // Compute bounding box diagonal length of lowest res shape model - double diagonalLength = - new BoundingBox(getLowResSmallBodyPolyData().GetBounds()).getDiagonalLength(); - double cubeSize = diagonalLength / 38.66056033363347; - smallBodyCubes = - new SmallBodyCubes(getLowResSmallBodyPolyData(), cubeSize, 0.01 * cubeSize, true); + /** + * Default constructor. Must be followed by a call to setSmallBodyPolyData. + */ + public SmallBodyModel() { + smallBodyPolyData = new vtkPolyData(); + genericCell = new vtkGenericCell(); + idList = new vtkIdList(); + idList2 = new vtkIdList(); } - return smallBodyCubes; - } + /** + * Convenience method for initializing a SmallBodyModel with just a vtkPolyData. + * + * @param polyData + */ + public SmallBodyModel(vtkPolyData polyData) { + this(); - public TreeSet getIntersectingCubes(vtkPolyData polydata) { - return getSmallBodyCubes().getIntersectingCubes(polydata); - } + vtkFloatArray[] coloringValues = {}; + String[] coloringNames = {}; + String[] coloringUnits = {}; + ColoringValueType coloringValueType = ColoringValueType.CELLDATA; - public TreeSet getIntersectingCubes(BoundingBox bb) { - return getSmallBodyCubes().getIntersectingCubes(bb); - } - - public int getCubeId(double[] point) { - return getSmallBodyCubes().getCubeId(point); - } - - public vtkFloatArray getCellNormals() { - // Compute the normals of necessary. For now don't add the normals to - // the cell - // data of the small body model since doing so might create problems. - // TODO consider adding normals to cell data without creating problems - if (cellNormals == null) { - vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); - normalsFilter.SetInputData(smallBodyPolyData); - normalsFilter.SetComputeCellNormals(1); - normalsFilter.SetComputePointNormals(0); - normalsFilter.SplittingOff(); - normalsFilter.ConsistencyOn(); - normalsFilter.AutoOrientNormalsOff(); - normalsFilter.Update(); - - vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); - vtkCellData normalsFilterOutputCellData = normalsFilterOutput.GetCellData(); - vtkFloatArray normals = (vtkFloatArray) normalsFilterOutputCellData.GetNormals(); - - cellNormals = new vtkFloatArray(); - cellNormals.DeepCopy(normals); - - normals.Delete(); - normalsFilterOutputCellData.Delete(); - normalsFilterOutput.Delete(); - normalsFilter.Delete(); + setSmallBodyPolyData(polyData, coloringValues, coloringNames, coloringUnits, coloringValueType); } - return cellNormals; - } + public void setSmallBodyPolyData( + vtkPolyData polydata, + vtkFloatArray[] coloringValues, + String[] coloringNames, + String[] coloringUnits, + ColoringValueType coloringValueType) { + smallBodyPolyData.DeepCopy(polydata); - /** - * Get the normal at a point. The normal vector at several vertices near the current point are - * averaged to compute the normal. - * - * @param point - * @return - */ - public double[] getNormalAtPoint(double[] point) { - return PolyDataUtil.getPolyDataNormalAtPoint(point, smallBodyPolyData, pointLocator); - } + smallBodyPolyData.BuildLinks(0); - /** - * Get the normal at a point. Unlike the other function with the same name, this averages the - * normals to all vertices within radius distance of point. - * - * @param point - * @param radius - * @return - */ - public double[] getNormalAtPoint(double[] point, double radius) { - return PolyDataUtil.getPolyDataNormalAtPointWithinRadius(point, smallBodyPolyData, pointLocator, - radius); - } + initializeLocators(); - public double[] getClosestNormal(double[] point) { - long closestCell = findClosestCell(point); - return getCellNormals().GetTuple3(closestCell); - } - - /** - * Get the normal at a point. Unlike the other function with the same name, this averages the - * normals to all vertices within radius distance of point. - * - * @param point - * @param radius - * @return - */ - public vtkIdList getIDsNearPoint(double[] point, double radius) { - return PolyDataUtil.getIDsAtPointWithinRadius(point, smallBodyPolyData, pointLocator, radius); - } - - /** - * This returns the closest point to the model to pt. Note the returned point need not be a vertex - * of the model and can lie anywhere on a plate. - * - * @param pt - * @return - */ - public double[] findClosestPoint(double[] pt) { - double[] closestPoint = new double[3]; - long[] cellId = new long[1]; - int[] subId = new int[1]; - double[] dist2 = new double[1]; - - cellLocator.FindClosestPoint(pt, closestPoint, genericCell, cellId, subId, dist2); - - return closestPoint; - } - - /** - * This returns the closest vertex in the shape model to pt. Unlike findClosestPoin this functions - * only returns one of the vertices of the shape model not an arbitrary point lying on a cell. - * - * @param pt - * @return - */ - public double[] findClosestVertex(double[] pt) { - long id = pointLocator.FindClosestPoint(pt); - double[] returnPt = new double[3]; - smallBodyPolyData.GetPoint(id, returnPt); - return returnPt; - } - - /** - * This returns the index of the closest cell in the model to pt. The closest point within the - * cell is returned in closestPoint - * - * @param pt - * @param closestPoint the closest point within the cell is returned here - * @return - */ - public long findClosestCell(double[] pt, double[] closestPoint) { - long[] cellId = new long[1]; - int[] subId = new int[1]; - double[] dist2 = new double[1]; - - // Use FindClosestPoint rather the FindCell since not sure what - // tolerance to use in the latter. - cellLocator.FindClosestPoint(pt, closestPoint, genericCell, cellId, subId, dist2); - - return cellId[0]; - } - - /** - * This returns the index of the closest cell in the model to pt. - * - * @param pt - * @return - */ - public long findClosestCell(double[] pt) { - double[] closestPoint = new double[3]; - return findClosestCell(pt, closestPoint); - } - - public Set findClosestCellsWithinRadius(double[] pt, double radius) { - Set cells = new HashSet<>(); - pointLocator.FindPointsWithinRadius(radius, pt, idList); - long size = idList.GetNumberOfIds(); - for (int i = 0; i < size; ++i) { - long id = idList.GetId(i); - smallBodyPolyData.GetPointCells(id, idList2); - long numCells = idList2.GetNumberOfIds(); - for (int j = 0; j < numCells; ++j) { - long id2 = idList2.GetId(j); - cells.add(id2); - } + lowResSmallBodyPolyData = smallBodyPolyData; + lowResPointLocator = pointLocator; } - return cells; - } - public ArrayList findClosestVerticesWithinRadius(double[] pt, double radius) { - ArrayList vertices = new ArrayList<>(); - pointLocator.FindPointsWithinRadius(radius, pt, idList); - long size = idList.GetNumberOfIds(); - for (long i = 0; i < size; ++i) { - long id = idList.GetId(i); - vertices.add(id); + public boolean isBuiltIn() { + return true; } - return vertices; - } - /** - * Compute the point on the asteroid that has the specified latitude and longitude. Returns the - * cell id of the cell containing that point. This is done by shooting a ray from the origin in - * the specified direction. - * - * @param lat - in radians - * @param lon - in radians - * @param intersectPoint - * @return the cellId of the cell containing the intersect point - */ - public long getPointAndCellIdFromLatLon(double lat, double lon, double[] intersectPoint) { - LatitudinalVector lla = new LatitudinalVector(1.0, lat, lon); - UnwritableVectorIJK rect = CoordConverters.convert(lla); - - double[] origin = {0.0, 0.0, 0.0}; - double[] lookPt = {rect.getI(), rect.getJ(), rect.getK()}; - - return computeRayIntersection(origin, lookPt, 10 * getBoundingBoxDiagonalLength(), - intersectPoint); - } - - /** - * Write shape out to regular rectangular lat/lon grid. Grid is 180 degrees in lat, 360 in lon. - * - * @param pixelsPerDegree - * @return - */ - public double[][][] polyDataToLatLonRadGrid(double pixelsPerDegree) { - int numRows = (int) Math.round(180.0 * pixelsPerDegree) + 1; - int numCols = (int) Math.round(360.0 * pixelsPerDegree) + 1; - double[][][] data = new double[6][numRows][numCols]; - - double[] intersectPoint = new double[3]; - - double incr = 1.0 / pixelsPerDegree; - for (int m = 0; m < numRows; ++m) { - for (int n = 0; n < numCols; ++n) { - double lat = m * incr - 90.0; - double lon = n * incr - 180.0; - - data[0][m][n] = lat; - data[1][m][n] = lon; - - long cellId = - getPointAndCellIdFromLatLon(Math.toRadians(lat), Math.toRadians(lon), intersectPoint); - double rad = -1.0e32; - if (cellId >= 0) - rad = new VectorIJK(intersectPoint).getLength(); - else { - logger.info(String.format("Warning: no intersection at lat:%.5f, lon:%.5f", lat, lon)); + private void initializeLocators() { + if (cellLocator == null) { + cellLocator = new vtksbCellLocator(); + bspLocator = new vtksbModifiedBSPTree(); + pointLocator = new vtkOctreePointLocator(); } - data[2][m][n] = rad; - data[3][m][n] = intersectPoint[0]; - data[4][m][n] = intersectPoint[1]; - data[5][m][n] = intersectPoint[2]; - } + + // Initialize the cell locator + cellLocator.FreeSearchStructure(); + cellLocator.SetDataSet(smallBodyPolyData); + cellLocator.CacheCellBoundsOn(); + cellLocator.AutomaticOn(); + // cellLocator.SetMaxLevel(10); + // cellLocator.SetNumberOfCellsPerNode(5); + cellLocator.BuildLocator(); + + // Initialize the BSP locator + bspLocator.FreeSearchStructure(); + bspLocator.SetDataSet(smallBodyPolyData); + bspLocator.CacheCellBoundsOn(); + bspLocator.AutomaticOn(); + // bspLocator.SetMaxLevel(10); + // bspLocator.SetNumberOfCellsPerNode(5); + bspLocator.BuildLocator(); + + pointLocator.FreeSearchStructure(); + pointLocator.SetDataSet(smallBodyPolyData); + pointLocator.BuildLocator(); } - return data; - } + private void initializeLowResData() { + if (lowResPointLocator == null) { + lowResSmallBodyPolyData = new vtkPolyData(); - /** - * Compute the intersection of a line segment with the asteroid. Returns the cell id of the cell - * containing that point. This is done by shooting a ray from the specified origin in the - * specified direction. - * - * @param origin one end of line segment - * @param direction direction of line segment, assumed to be a unit vector - * @param intersectPoint (returned) - * @return the cellId of the cell containing the intersect point or -1 if no intersection - */ - public long computeRayIntersection(double[] origin, double[] direction, double[] intersectPoint) { - double distance = new VectorIJK(origin).getLength() + 10 * getBoundingBoxDiagonalLength(); - return computeRayIntersection(origin, direction, distance, intersectPoint); - } + try { + lowResSmallBodyPolyData.ShallowCopy( + PolyDataUtil.loadShapeModelAndComputeNormals(defaultModelFile.getAbsolutePath())); + } catch (Exception e) { + e.printStackTrace(); + } - /** - * Compute the intersection of a line segment with the asteroid. Returns the cell id of the cell - * containing that point. This is done by shooting a ray from the specified origin in the - * specified direction. - * - * @param origin one end of line segment - * @param direction direction of line segment, assumed to be a unit vector - * @param distance length of line segment - * @param intersectPoint (returned) - * @return the cellId of the cell containing the intersect point or -1 if no intersection - */ - public long computeRayIntersection(double[] origin, double[] direction, double distance, - double[] intersectPoint) { - - double[] lookPt = new double[3]; - lookPt[0] = origin[0] + 2.0 * distance * direction[0]; - lookPt[1] = origin[1] + 2.0 * distance * direction[1]; - lookPt[2] = origin[2] + 2.0 * distance * direction[2]; - - double tol = 1e-6; - double[] t = new double[1]; - double[] x = new double[3]; - double[] pcoords = new double[3]; - int[] subId = new int[1]; - long[] cellId = new long[1]; - - int result = cellLocator.IntersectWithLine(origin, lookPt, tol, t, x, pcoords, subId, cellId, - genericCell); - - intersectPoint[0] = x[0]; - intersectPoint[1] = x[1]; - intersectPoint[2] = x[2]; - - if (result > 0) - return cellId[0]; - else - return -1; - } - - /** - * Return a unit vector that points east - * - * @param pt direction of the surface point from the origin. Does not need to be a unit vector or - * lie on the surface. - * @return unit vector that points east - */ - public Vector3D findEastVector(double[] pt) { - // define a topographic frame where the Z axis points up and the Y axis points north. The X axis - // will point east. - Rotation bodyFixedToTopo = - RotationUtils.KprimaryJsecondary(new Vector3D(pt), Vector3D.PLUS_K); - return bodyFixedToTopo.applyTo(Vector3D.PLUS_I); - } - - /** - * Return a unit vector that points west - * - * @param pt direction of the surface point from the origin. Does not need to be a unit vector or - * lie on the surface. - * @return unit vector that points west - */ - public Vector3D findWestVector(double[] pt) { - // define a topographic frame where the Z axis points up and the Y axis points north. The X axis - // will point east. - Rotation bodyFixedToTopo = - RotationUtils.KprimaryJsecondary(new Vector3D(pt), Vector3D.PLUS_K); - return bodyFixedToTopo.applyTo(Vector3D.MINUS_I); - } - - /** @return {@link BoundingBox} which encloses this shape */ - public BoundingBox getBoundingBox() { - if (boundingBox == null) { - smallBodyPolyData.ComputeBounds(); - boundingBox = new BoundingBox(smallBodyPolyData.GetBounds()); + lowResPointLocator = new vtkOctreePointLocator(); + lowResPointLocator.SetDataSet(lowResSmallBodyPolyData); + lowResPointLocator.BuildLocator(); + } } - return boundingBox; - } - - /** @return diagonal length of the enclosing @{link BoundingBox} */ - public double getBoundingBoxDiagonalLength() { - return getBoundingBox().getDiagonalLength(); - } - - /** @return statistics on the edge lengths of each cell of the shape model */ - public DescriptiveStatistics computeLargestSmallestMeanEdgeLength() { - long numberOfCells = smallBodyPolyData.GetNumberOfCells(); - - DescriptiveStatistics stats = new DescriptiveStatistics(); - for (int i = 0; i < numberOfCells; ++i) { - vtkCell cell = smallBodyPolyData.GetCell(i); - vtkPoints points = cell.GetPoints(); - double[] pt0 = points.GetPoint(0); - double[] pt1 = points.GetPoint(1); - double[] pt2 = points.GetPoint(2); - - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - - stats.addValue(VectorIJK.subtract(facet.getVertex1(), facet.getVertex2()).getLength()); - stats.addValue(VectorIJK.subtract(facet.getVertex2(), facet.getVertex3()).getLength()); - stats.addValue(VectorIJK.subtract(facet.getVertex3(), facet.getVertex1()).getLength()); - - points.Delete(); - cell.Delete(); + public vtkPolyData getSmallBodyPolyData() { + return smallBodyPolyData; } - return stats; - } + public vtkPolyData getLowResSmallBodyPolyData() { + initializeLowResData(); - public String getModelName() { - if (resolutionLevel >= 0 && resolutionLevel < modelNames.length) - return modelNames[resolutionLevel]; - else - return null; - } + return lowResSmallBodyPolyData; + } - /** clean up VTK allocated internal objects */ - public void delete() { - if (cellLocator != null) - cellLocator.Delete(); - if (bspLocator != null) - bspLocator.Delete(); - if (pointLocator != null) - pointLocator.Delete(); - if (genericCell != null) - genericCell.Delete(); - if (smallBodyPolyData != null) - smallBodyPolyData.Delete(); - } + public vtksbCellLocator getCellLocator() { + return cellLocator; + } - public void saveAsPLT(File file) throws IOException { - PolyDataUtil.saveShapeModelAsPLT(smallBodyPolyData, file.getAbsolutePath()); - } + public vtkAbstractPointLocator getPointLocator() { + return pointLocator; + } - public void saveAsOBJ(File file) throws IOException { - PolyDataUtil.saveShapeModelAsOBJ(smallBodyPolyData, file.getAbsolutePath()); - } + public SmallBodyCubes getSmallBodyCubes() { + if (smallBodyCubes == null) { + // The number 38.66056033363347 is used here so that the cube size + // comes out to 1 km for Eros. - public void saveAsVTK(File file) throws IOException { - PolyDataUtil.saveShapeModelAsVTK(smallBodyPolyData, file.getAbsolutePath()); - } + // Compute bounding box diagonal length of lowest res shape model + double diagonalLength = new BoundingBox(getLowResSmallBodyPolyData().GetBounds()).getDiagonalLength(); + double cubeSize = diagonalLength / 38.66056033363347; + smallBodyCubes = new SmallBodyCubes(getLowResSmallBodyPolyData(), cubeSize, 0.01 * cubeSize, true); + } - public void saveAsSTL(File file) throws IOException { - PolyDataUtil.saveShapeModelAsSTL(smallBodyPolyData, file.getAbsolutePath()); - } + return smallBodyCubes; + } + public TreeSet getIntersectingCubes(vtkPolyData polydata) { + return getSmallBodyCubes().getIntersectingCubes(polydata); + } + + public TreeSet getIntersectingCubes(BoundingBox bb) { + return getSmallBodyCubes().getIntersectingCubes(bb); + } + + public int getCubeId(double[] point) { + return getSmallBodyCubes().getCubeId(point); + } + + public vtkFloatArray getCellNormals() { + // Compute the normals of necessary. For now don't add the normals to + // the cell + // data of the small body model since doing so might create problems. + // TODO consider adding normals to cell data without creating problems + if (cellNormals == null) { + vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); + normalsFilter.SetInputData(smallBodyPolyData); + normalsFilter.SetComputeCellNormals(1); + normalsFilter.SetComputePointNormals(0); + normalsFilter.SplittingOff(); + normalsFilter.ConsistencyOn(); + normalsFilter.AutoOrientNormalsOff(); + normalsFilter.Update(); + + vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); + vtkCellData normalsFilterOutputCellData = normalsFilterOutput.GetCellData(); + vtkFloatArray normals = (vtkFloatArray) normalsFilterOutputCellData.GetNormals(); + + cellNormals = new vtkFloatArray(); + cellNormals.DeepCopy(normals); + + normals.Delete(); + normalsFilterOutputCellData.Delete(); + normalsFilterOutput.Delete(); + normalsFilter.Delete(); + } + + return cellNormals; + } + + /** + * Get the normal at a point. The normal vector at several vertices near the current point are + * averaged to compute the normal. + * + * @param point + * @return + */ + public double[] getNormalAtPoint(double[] point) { + return PolyDataUtil.getPolyDataNormalAtPoint(point, smallBodyPolyData, pointLocator); + } + + /** + * Get the normal at a point. Unlike the other function with the same name, this averages the + * normals to all vertices within radius distance of point. + * + * @param point + * @param radius + * @return + */ + public double[] getNormalAtPoint(double[] point, double radius) { + return PolyDataUtil.getPolyDataNormalAtPointWithinRadius(point, smallBodyPolyData, pointLocator, radius); + } + + public double[] getClosestNormal(double[] point) { + long closestCell = findClosestCell(point); + return getCellNormals().GetTuple3(closestCell); + } + + /** + * Get the normal at a point. Unlike the other function with the same name, this averages the + * normals to all vertices within radius distance of point. + * + * @param point + * @param radius + * @return + */ + public vtkIdList getIDsNearPoint(double[] point, double radius) { + return PolyDataUtil.getIDsAtPointWithinRadius(point, smallBodyPolyData, pointLocator, radius); + } + + /** + * This returns the closest point to the model to pt. Note the returned point need not be a vertex + * of the model and can lie anywhere on a plate. + * + * @param pt + * @return + */ + public double[] findClosestPoint(double[] pt) { + double[] closestPoint = new double[3]; + long[] cellId = new long[1]; + int[] subId = new int[1]; + double[] dist2 = new double[1]; + + cellLocator.FindClosestPoint(pt, closestPoint, genericCell, cellId, subId, dist2); + + return closestPoint; + } + + /** + * This returns the closest vertex in the shape model to pt. Unlike findClosestPoin this functions + * only returns one of the vertices of the shape model not an arbitrary point lying on a cell. + * + * @param pt + * @return + */ + public double[] findClosestVertex(double[] pt) { + long id = pointLocator.FindClosestPoint(pt); + double[] returnPt = new double[3]; + smallBodyPolyData.GetPoint(id, returnPt); + return returnPt; + } + + /** + * This returns the index of the closest cell in the model to pt. The closest point within the + * cell is returned in closestPoint + * + * @param pt + * @param closestPoint the closest point within the cell is returned here + * @return + */ + public long findClosestCell(double[] pt, double[] closestPoint) { + long[] cellId = new long[1]; + int[] subId = new int[1]; + double[] dist2 = new double[1]; + + // Use FindClosestPoint rather the FindCell since not sure what + // tolerance to use in the latter. + cellLocator.FindClosestPoint(pt, closestPoint, genericCell, cellId, subId, dist2); + + return cellId[0]; + } + + /** + * This returns the index of the closest cell in the model to pt. + * + * @param pt + * @return + */ + public long findClosestCell(double[] pt) { + double[] closestPoint = new double[3]; + return findClosestCell(pt, closestPoint); + } + + public Set findClosestCellsWithinRadius(double[] pt, double radius) { + Set cells = new HashSet<>(); + pointLocator.FindPointsWithinRadius(radius, pt, idList); + long size = idList.GetNumberOfIds(); + for (int i = 0; i < size; ++i) { + long id = idList.GetId(i); + smallBodyPolyData.GetPointCells(id, idList2); + long numCells = idList2.GetNumberOfIds(); + for (int j = 0; j < numCells; ++j) { + long id2 = idList2.GetId(j); + cells.add(id2); + } + } + return cells; + } + + public ArrayList findClosestVerticesWithinRadius(double[] pt, double radius) { + ArrayList vertices = new ArrayList<>(); + pointLocator.FindPointsWithinRadius(radius, pt, idList); + long size = idList.GetNumberOfIds(); + for (long i = 0; i < size; ++i) { + long id = idList.GetId(i); + vertices.add(id); + } + return vertices; + } + + /** + * Compute the point on the asteroid that has the specified latitude and longitude. Returns the + * cell id of the cell containing that point. This is done by shooting a ray from the origin in + * the specified direction. + * + * @param lat - in radians + * @param lon - in radians + * @param intersectPoint + * @return the cellId of the cell containing the intersect point + */ + public long getPointAndCellIdFromLatLon(double lat, double lon, double[] intersectPoint) { + LatitudinalVector lla = new LatitudinalVector(1.0, lat, lon); + UnwritableVectorIJK rect = CoordConverters.convert(lla); + + double[] origin = {0.0, 0.0, 0.0}; + double[] lookPt = {rect.getI(), rect.getJ(), rect.getK()}; + + return computeRayIntersection(origin, lookPt, 10 * getBoundingBoxDiagonalLength(), intersectPoint); + } + + /** + * Write shape out to regular rectangular lat/lon grid. Grid is 180 degrees in lat, 360 in lon. + * + * @param pixelsPerDegree + * @return + */ + public double[][][] polyDataToLatLonRadGrid(double pixelsPerDegree) { + int numRows = (int) Math.round(180.0 * pixelsPerDegree) + 1; + int numCols = (int) Math.round(360.0 * pixelsPerDegree) + 1; + double[][][] data = new double[6][numRows][numCols]; + + double[] intersectPoint = new double[3]; + + double incr = 1.0 / pixelsPerDegree; + for (int m = 0; m < numRows; ++m) { + for (int n = 0; n < numCols; ++n) { + double lat = m * incr - 90.0; + double lon = n * incr - 180.0; + + data[0][m][n] = lat; + data[1][m][n] = lon; + + long cellId = getPointAndCellIdFromLatLon(Math.toRadians(lat), Math.toRadians(lon), intersectPoint); + double rad = -1.0e32; + if (cellId >= 0) rad = new VectorIJK(intersectPoint).getLength(); + else { + logger.info(String.format("Warning: no intersection at lat:%.5f, lon:%.5f", lat, lon)); + } + data[2][m][n] = rad; + data[3][m][n] = intersectPoint[0]; + data[4][m][n] = intersectPoint[1]; + data[5][m][n] = intersectPoint[2]; + } + } + + return data; + } + + /** + * Compute the intersection of a line segment with the asteroid. Returns the cell id of the cell + * containing that point. This is done by shooting a ray from the specified origin in the + * specified direction. + * + * @param origin one end of line segment + * @param direction direction of line segment, assumed to be a unit vector + * @param intersectPoint (returned) + * @return the cellId of the cell containing the intersect point or -1 if no intersection + */ + public long computeRayIntersection(double[] origin, double[] direction, double[] intersectPoint) { + double distance = new VectorIJK(origin).getLength() + 10 * getBoundingBoxDiagonalLength(); + return computeRayIntersection(origin, direction, distance, intersectPoint); + } + + /** + * Compute the intersection of a line segment with the asteroid. Returns the cell id of the cell + * containing that point. This is done by shooting a ray from the specified origin in the + * specified direction. + * + * @param origin one end of line segment + * @param direction direction of line segment, assumed to be a unit vector + * @param distance length of line segment + * @param intersectPoint (returned) + * @return the cellId of the cell containing the intersect point or -1 if no intersection + */ + public long computeRayIntersection(double[] origin, double[] direction, double distance, double[] intersectPoint) { + + double[] lookPt = new double[3]; + lookPt[0] = origin[0] + 2.0 * distance * direction[0]; + lookPt[1] = origin[1] + 2.0 * distance * direction[1]; + lookPt[2] = origin[2] + 2.0 * distance * direction[2]; + + double tol = 1e-6; + double[] t = new double[1]; + double[] x = new double[3]; + double[] pcoords = new double[3]; + int[] subId = new int[1]; + long[] cellId = new long[1]; + + int result = cellLocator.IntersectWithLine(origin, lookPt, tol, t, x, pcoords, subId, cellId, genericCell); + + intersectPoint[0] = x[0]; + intersectPoint[1] = x[1]; + intersectPoint[2] = x[2]; + + if (result > 0) return cellId[0]; + else return -1; + } + + /** + * Return a unit vector that points east + * + * @param pt direction of the surface point from the origin. Does not need to be a unit vector or + * lie on the surface. + * @return unit vector that points east + */ + public Vector3D findEastVector(double[] pt) { + // define a topographic frame where the Z axis points up and the Y axis points north. The X axis + // will point east. + Rotation bodyFixedToTopo = RotationUtils.KprimaryJsecondary(new Vector3D(pt), Vector3D.PLUS_K); + return bodyFixedToTopo.applyTo(Vector3D.PLUS_I); + } + + /** + * Return a unit vector that points west + * + * @param pt direction of the surface point from the origin. Does not need to be a unit vector or + * lie on the surface. + * @return unit vector that points west + */ + public Vector3D findWestVector(double[] pt) { + // define a topographic frame where the Z axis points up and the Y axis points north. The X axis + // will point east. + Rotation bodyFixedToTopo = RotationUtils.KprimaryJsecondary(new Vector3D(pt), Vector3D.PLUS_K); + return bodyFixedToTopo.applyTo(Vector3D.MINUS_I); + } + + /** @return {@link BoundingBox} which encloses this shape */ + public BoundingBox getBoundingBox() { + if (boundingBox == null) { + smallBodyPolyData.ComputeBounds(); + boundingBox = new BoundingBox(smallBodyPolyData.GetBounds()); + } + + return boundingBox; + } + + /** @return diagonal length of the enclosing @{link BoundingBox} */ + public double getBoundingBoxDiagonalLength() { + return getBoundingBox().getDiagonalLength(); + } + + /** @return statistics on the edge lengths of each cell of the shape model */ + public DescriptiveStatistics computeLargestSmallestMeanEdgeLength() { + long numberOfCells = smallBodyPolyData.GetNumberOfCells(); + + DescriptiveStatistics stats = new DescriptiveStatistics(); + for (int i = 0; i < numberOfCells; ++i) { + vtkCell cell = smallBodyPolyData.GetCell(i); + vtkPoints points = cell.GetPoints(); + double[] pt0 = points.GetPoint(0); + double[] pt1 = points.GetPoint(1); + double[] pt2 = points.GetPoint(2); + + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + + stats.addValue( + VectorIJK.subtract(facet.getVertex1(), facet.getVertex2()).getLength()); + stats.addValue( + VectorIJK.subtract(facet.getVertex2(), facet.getVertex3()).getLength()); + stats.addValue( + VectorIJK.subtract(facet.getVertex3(), facet.getVertex1()).getLength()); + + points.Delete(); + cell.Delete(); + } + + return stats; + } + + public String getModelName() { + if (resolutionLevel >= 0 && resolutionLevel < modelNames.length) return modelNames[resolutionLevel]; + else return null; + } + + /** clean up VTK allocated internal objects */ + public void delete() { + if (cellLocator != null) cellLocator.Delete(); + if (bspLocator != null) bspLocator.Delete(); + if (pointLocator != null) pointLocator.Delete(); + if (genericCell != null) genericCell.Delete(); + if (smallBodyPolyData != null) smallBodyPolyData.Delete(); + } + + public void saveAsPLT(File file) throws IOException { + PolyDataUtil.saveShapeModelAsPLT(smallBodyPolyData, file.getAbsolutePath()); + } + + public void saveAsOBJ(File file) throws IOException { + PolyDataUtil.saveShapeModelAsOBJ(smallBodyPolyData, file.getAbsolutePath()); + } + + public void saveAsVTK(File file) throws IOException { + PolyDataUtil.saveShapeModelAsVTK(smallBodyPolyData, file.getAbsolutePath()); + } + + public void saveAsSTL(File file) throws IOException { + PolyDataUtil.saveShapeModelAsSTL(smallBodyPolyData, file.getAbsolutePath()); + } } diff --git a/src/main/java/terrasaur/templates/DefaultTerrasaurTool.java b/src/main/java/terrasaur/templates/DefaultTerrasaurTool.java index 226805c..ebb1307 100644 --- a/src/main/java/terrasaur/templates/DefaultTerrasaurTool.java +++ b/src/main/java/terrasaur/templates/DefaultTerrasaurTool.java @@ -36,51 +36,49 @@ import org.apache.logging.log4j.Logger; */ public class DefaultTerrasaurTool implements TerrasaurTool { - private static final Logger logger = LogManager.getLogger(); + 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 DefaultTerrasaurTool() {} + /** + * This doesn't need to be private, or even declared, but you might want to if you have other + * constructors. + */ + private DefaultTerrasaurTool() {} - @Override - public String shortDescription() { - return "SHORT DESCRIPTION."; - } + @Override + public String shortDescription() { + return "SHORT DESCRIPTION."; + } - @Override - public String fullDescription(Options options) { - String header = "TEXT APPEARING BEFORE COMMAND LINE OPTION SUMMARY"; - String footer = "\nTEXT APPENDED TO COMMAND LINE OPTION SUMMARY.\n"; - return TerrasaurTool.super.fullDescription(options, header, footer); - } + @Override + public String fullDescription(Options options) { + String header = "TEXT APPEARING BEFORE COMMAND LINE OPTION SUMMARY"; + String footer = "\nTEXT APPENDED TO COMMAND LINE OPTION SUMMARY.\n"; + return TerrasaurTool.super.fullDescription(options, header, footer); + } - private static Options defineOptions() { - Options options = TerrasaurTool.defineOptions(); - options.addOption( - Option.builder("env") - .hasArgs() - .required() - .desc("Print the named environment variable's value. Can take multiple arguments.") - .build()); - return options; - } + private static Options defineOptions() { + Options options = TerrasaurTool.defineOptions(); + options.addOption(Option.builder("env") + .hasArgs() + .required() + .desc("Print the named environment variable's value. Can take multiple arguments.") + .build()); + return options; + } - public static void main(String[] args) { - TerrasaurTool defaultOBJ = new DefaultTerrasaurTool(); + public static void main(String[] args) { + TerrasaurTool defaultOBJ = new DefaultTerrasaurTool(); - Options options = defineOptions(); + Options options = defineOptions(); - CommandLine cl = defaultOBJ.parseArgs(args, options); + CommandLine cl = defaultOBJ.parseArgs(args, options); - Map startupMessages = defaultOBJ.startupMessages(cl); - for (MessageLabel ml : startupMessages.keySet()) - logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); + Map startupMessages = defaultOBJ.startupMessages(cl); + for (MessageLabel ml : startupMessages.keySet()) + logger.info(String.format("%s %s", ml.label, startupMessages.get(ml))); - for (String env : cl.getOptionValues("env")) - logger.info(String.format("%s: %s", env, System.getenv(env))); + for (String env : cl.getOptionValues("env")) logger.info(String.format("%s: %s", env, System.getenv(env))); - logger.info("Finished"); - } + logger.info("Finished"); + } } diff --git a/src/main/java/terrasaur/templates/TerrasaurTool.java b/src/main/java/terrasaur/templates/TerrasaurTool.java index 2eeeae0..81b0ad6 100644 --- a/src/main/java/terrasaur/templates/TerrasaurTool.java +++ b/src/main/java/terrasaur/templates/TerrasaurTool.java @@ -22,17 +22,16 @@ */ package terrasaur.templates; -import org.apache.commons.cli.*; -import org.apache.logging.log4j.Level; -import terrasaur.utils.AppVersion; -import terrasaur.utils.Log4j2Configurator; - import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.LinkedHashMap; import java.util.Map; +import org.apache.commons.cli.*; +import org.apache.logging.log4j.Level; +import terrasaur.utils.AppVersion; +import terrasaur.utils.Log4j2Configurator; /** * All classes in the apps folder should implement this interface. Calling the class without @@ -43,188 +42,181 @@ import java.util.Map; */ public interface TerrasaurTool { - /** Show required options first, followed by non-required. */ - class CustomHelpFormatter extends HelpFormatter { - public CustomHelpFormatter() { - setOptionComparator( - (o1, o2) -> { + /** Show required options first, followed by non-required. */ + class CustomHelpFormatter extends HelpFormatter { + public CustomHelpFormatter() { + setOptionComparator((o1, o2) -> { if (o1.isRequired() && !o2.isRequired()) return -1; if (!o1.isRequired() && o2.isRequired()) return 1; return o1.getKey().compareToIgnoreCase(o2.getKey()); - }); - } - } - - /** - * @return One line description of this tool - */ - String shortDescription(); - - /** - * @param options command line options - * @param header String to print before argument list - * @param footer String to print after argument list - * @return Complete description of this tool. - */ - default String fullDescription(Options options, String header, String footer) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - - pw.println(AppVersion.getFullString() + "\n"); - HelpFormatter formatter = new CustomHelpFormatter(); - - formatter.printHelp( - pw, - formatter.getWidth(), - String.format("%s [options]", this.getClass().getSimpleName()), - header, - options, - formatter.getLeftPadding(), - formatter.getDescPadding(), - footer); - pw.flush(); - return sw.toString(); - } - - /** - * @param options command line options - * @return Complete description of this tool. - */ - default String fullDescription(Options options) { - return fullDescription(options, "", ""); - } - - /** - * @param args arguments to parse - * @param options set of options accepted by the program - * @return command line formed by parsing arguments - */ - default CommandLine parseArgs(String[] args, Options options) { - // if no arguments, print the usage and exit - if (args.length == 0) { - System.out.println(fullDescription(options)); - System.exit(0); + }); + } } - // if -shortDescription is specified, print short description and exit. - for (String arg : args) { - if (arg.equals("-shortDescription")) { - System.out.println(shortDescription()); - System.exit(0); - } + /** + * @return One line description of this tool + */ + String shortDescription(); + + /** + * @param options command line options + * @param header String to print before argument list + * @param footer String to print after argument list + * @return Complete description of this tool. + */ + default String fullDescription(Options options, String header, String footer) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + pw.println(AppVersion.getFullString() + "\n"); + HelpFormatter formatter = new CustomHelpFormatter(); + + formatter.printHelp( + pw, + formatter.getWidth(), + String.format("%s [options]", this.getClass().getSimpleName()), + header, + options, + formatter.getLeftPadding(), + formatter.getDescPadding(), + footer); + pw.flush(); + return sw.toString(); } - // parse the arguments - CommandLine cl = null; - try { - cl = new DefaultParser().parse(options, args); - } catch (ParseException e) { - System.out.println(e.getMessage()); - System.out.println(fullDescription(options)); - System.exit(0); + /** + * @param options command line options + * @return Complete description of this tool. + */ + default String fullDescription(Options options) { + return fullDescription(options, "", ""); } - return cl; - } + /** + * @param args arguments to parse + * @param options set of options accepted by the program + * @return command line formed by parsing arguments + */ + default CommandLine parseArgs(String[] args, Options options) { + // if no arguments, print the usage and exit + if (args.length == 0) { + System.out.println(fullDescription(options)); + System.exit(0); + } - /** - * @return options including -logFile and -logLevel - */ - static Options defineOptions() { - Options options = new Options(); - options.addOption( - Option.builder("logFile") - .hasArg() - .desc("If present, save screen output to log file.") - .build()); - StringBuilder sb = new StringBuilder(); - for (Level l : Level.values()) sb.append(String.format("%s ", l.name())); - options.addOption( - Option.builder("logLevel") - .hasArg() - .desc( - "If present, print messages above selected priority. Valid values are " - + sb.toString().trim() - + ". Default is INFO.") - .build()); - return options; - } + // if -shortDescription is specified, print short description and exit. + for (String arg : args) { + if (arg.equals("-shortDescription")) { + System.out.println(shortDescription()); + System.exit(0); + } + } - /** Labels for startup messages. The enum order is the order in which they are printed. */ - enum MessageLabel { - START("Start"), - ARGUMENTS("arguments"); - public final String label; + // parse the arguments + CommandLine cl = null; + try { + cl = new DefaultParser().parse(options, args); + } catch (ParseException e) { + System.out.println(e.getMessage()); + System.out.println(fullDescription(options)); + System.exit(0); + } - MessageLabel(String label) { - this.label = label; - } - } - - /** - * Generate startup messages. This is returned as a map. For example: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
KeyValue
- * Start - * - * MEGANESimulator [MMXTools version 25.01.28-b868ef6M] on nairah1-ml1 - *
arguments:-spice /project/sis/users/nairah1/MMX/spice/meganeLCp.mk -startTime 2026 JUN 20 00:00:00 -stopTime 2026 JUN 20 08:00:00 -delta 180 -outputCSV tmp.csv -numThreads 1 -obj /project/sis/users/nairah1/MMX/obj/Phobos-Ernst-800.obj -dbName tmp.db
- * - * @param cl Command line object - * @return standard startup messages - */ - default Map startupMessages(CommandLine cl) { - - Map startupMessages = new LinkedHashMap<>(); - - String hostname = "unknown host"; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException ignored) { + return cl; } - Log4j2Configurator lc = Log4j2Configurator.getInstance(); - if (cl.hasOption("logLevel")) - lc.setLevel(Level.valueOf(cl.getOptionValue("logLevel").toUpperCase().trim())); - - if (cl.hasOption("logFile")) lc.addFile(cl.getOptionValue("logFile")); - - StringBuilder sb = - new StringBuilder( - String.format( - "%s [%s] on %s", - getClass().getSimpleName(), AppVersion.getVersionString(), hostname)); - startupMessages.put(MessageLabel.START, sb.toString()); - sb = new StringBuilder(); - - for (Option option : cl.getOptions()) { - sb.append("-").append(option.getOpt()).append(" "); - if (option.hasArgs()) { - for (String arg : option.getValues()) sb.append(arg).append(" "); - } else if (option.hasArg()) { - sb.append(option.getValue()).append(" "); - } - } - for (String arg : cl.getArgs()) { - sb.append(arg).append(" "); + /** + * @return options including -logFile and -logLevel + */ + static Options defineOptions() { + Options options = new Options(); + options.addOption(Option.builder("logFile") + .hasArg() + .desc("If present, save screen output to log file.") + .build()); + StringBuilder sb = new StringBuilder(); + for (Level l : Level.values()) sb.append(String.format("%s ", l.name())); + options.addOption(Option.builder("logLevel") + .hasArg() + .desc("If present, print messages above selected priority. Valid values are " + + sb.toString().trim() + + ". Default is INFO.") + .build()); + return options; } - startupMessages.put(MessageLabel.ARGUMENTS, sb.toString()); + /** Labels for startup messages. The enum order is the order in which they are printed. */ + enum MessageLabel { + START("Start"), + ARGUMENTS("arguments"); + public final String label; - return startupMessages; - } + MessageLabel(String label) { + this.label = label; + } + } + /** + * Generate startup messages. This is returned as a map. For example: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
KeyValue
+ * Start + * + * MEGANESimulator [MMXTools version 25.01.28-b868ef6M] on nairah1-ml1 + *
arguments:-spice /project/sis/users/nairah1/MMX/spice/meganeLCp.mk -startTime 2026 JUN 20 00:00:00 -stopTime 2026 JUN 20 08:00:00 -delta 180 -outputCSV tmp.csv -numThreads 1 -obj /project/sis/users/nairah1/MMX/obj/Phobos-Ernst-800.obj -dbName tmp.db
+ * + * @param cl Command line object + * @return standard startup messages + */ + default Map startupMessages(CommandLine cl) { + + Map startupMessages = new LinkedHashMap<>(); + + String hostname = "unknown host"; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException ignored) { + } + + Log4j2Configurator lc = Log4j2Configurator.getInstance(); + if (cl.hasOption("logLevel")) + lc.setLevel( + Level.valueOf(cl.getOptionValue("logLevel").toUpperCase().trim())); + + if (cl.hasOption("logFile")) lc.addFile(cl.getOptionValue("logFile")); + + StringBuilder sb = new StringBuilder( + String.format("%s [%s] on %s", getClass().getSimpleName(), AppVersion.getVersionString(), hostname)); + startupMessages.put(MessageLabel.START, sb.toString()); + sb = new StringBuilder(); + + for (Option option : cl.getOptions()) { + sb.append("-").append(option.getOpt()).append(" "); + if (option.hasArgs()) { + for (String arg : option.getValues()) sb.append(arg).append(" "); + } else if (option.hasArg()) { + sb.append(option.getValue()).append(" "); + } + } + for (String arg : cl.getArgs()) { + sb.append(arg).append(" "); + } + + startupMessages.put(MessageLabel.ARGUMENTS, sb.toString()); + + return startupMessages; + } } diff --git a/src/main/java/terrasaur/utils/AppVersion.java b/src/main/java/terrasaur/utils/AppVersion.java index 4d73e5a..4a0acfa 100644 --- a/src/main/java/terrasaur/utils/AppVersion.java +++ b/src/main/java/terrasaur/utils/AppVersion.java @@ -24,26 +24,25 @@ package terrasaur.utils; public class AppVersion { - public final static String lastCommit = "25.04.27"; + public static final String lastCommit = "25.07.30"; // an M at the end of gitRevision means this was built from a "dirty" git repository - public final static String gitRevision = "cb0f7f8"; - public final static String applicationName = "Terrasaur"; - public final static String dateString = "2025-Apr-28 15:06:13 UTC"; + public static final String gitRevision = "6212144"; + public static final String applicationName = "Terrasaur"; + public static final String dateString = "2025-Jul-30 16:05:45 UTC"; - private AppVersion() {} + private AppVersion() {} /** - * Terrasaur version 25.04.27-cb0f7f8 built 2025-Apr-28 15:06:13 UTC + * Terrasaur version 25.07.30-6212144 built 2025-Jul-30 16:05:45 UTC */ public static String getFullString() { - return String.format("%s version %s-%s built %s", applicationName, lastCommit, gitRevision, dateString); + return String.format("%s version %s-%s built %s", applicationName, lastCommit, gitRevision, dateString); } /** - * Terrasaur version 25.04.27-cb0f7f8 + * Terrasaur version 25.07.30-6212144 */ public static String getVersionString() { - return String.format("%s version %s-%s", applicationName, lastCommit, gitRevision); + return String.format("%s version %s-%s", applicationName, lastCommit, gitRevision); } } - diff --git a/src/main/java/terrasaur/utils/Binary16.java b/src/main/java/terrasaur/utils/Binary16.java index a7e0cfc..c5d3992 100644 --- a/src/main/java/terrasaur/utils/Binary16.java +++ b/src/main/java/terrasaur/utils/Binary16.java @@ -29,76 +29,78 @@ package terrasaur.utils; * From Stack * Overflow - * + * * @author nairah1 * */ public class Binary16 { - /** - * Calculate a floating point value from the lower 16 bits of a 32 bit integer. The upper 16 bits - * are ignored. - * - * @param hbits - * @return - */ - // ignores the higher 16 bits - public static float toFloat(int hbits) { - int mant = hbits & 0x03ff; // 10 bits mantissa - int exp = hbits & 0x7c00; // 5 bits exponent - if (exp == 0x7c00) // NaN/Inf - exp = 0x3fc00; // -> NaN/Inf - else if (exp != 0) // normalized value - { - exp += 0x1c000; // exp - 15 + 127 - if (mant == 0 && exp > 0x1c400) // smooth transition - return Float.intBitsToFloat((hbits & 0x8000) << 16 | exp << 13 | 0x3ff); - } else if (mant != 0) // && exp==0 -> subnormal - { - exp = 0x1c400; // make it normal - do { - mant <<= 1; // mantissa * 2 - exp -= 0x400; // decrease exp by 1 - } while ((mant & 0x400) == 0); // while not normal - mant &= 0x3ff; // discard subnormal bit - } // else +/-0 -> +/-0 - return Float.intBitsToFloat( // combine all parts - (hbits & 0x8000) << 16 // sign << ( 31 - 15 ) - | (exp | mant) << 13); // value << ( 23 - 10 ) - } - - /** - * Calculate a 16 bit representation of a floating point value. The upper 16 bits of the result - * are 0. - * - * - * @param fval - * @return - */ - // returns all higher 16 bits as 0 for all results - public static int fromFloat(float fval) { - int fbits = Float.floatToIntBits(fval); - int sign = fbits >>> 16 & 0x8000; // sign only - int val = (fbits & 0x7fffffff) + 0x1000; // rounded value - - if (val >= 0x47800000) // might be or become NaN/Inf - { // avoid Inf due to rounding - if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf - if (val < 0x7f800000) // was value but too large - return sign | 0x7c00; // make it +/-Inf - return sign | 0x7c00 | // remains +/-Inf or NaN - (fbits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits - } - return sign | 0x7bff; // unrounded not quite Inf + /** + * Calculate a floating point value from the lower 16 bits of a 32 bit integer. The upper 16 bits + * are ignored. + * + * @param hbits + * @return + */ + // ignores the higher 16 bits + public static float toFloat(int hbits) { + int mant = hbits & 0x03ff; // 10 bits mantissa + int exp = hbits & 0x7c00; // 5 bits exponent + if (exp == 0x7c00) // NaN/Inf + exp = 0x3fc00; // -> NaN/Inf + else if (exp != 0) // normalized value + { + exp += 0x1c000; // exp - 15 + 127 + if (mant == 0 && exp > 0x1c400) // smooth transition + return Float.intBitsToFloat((hbits & 0x8000) << 16 | exp << 13 | 0x3ff); + } else if (mant != 0) // && exp==0 -> subnormal + { + exp = 0x1c400; // make it normal + do { + mant <<= 1; // mantissa * 2 + exp -= 0x400; // decrease exp by 1 + } while ((mant & 0x400) == 0); // while not normal + mant &= 0x3ff; // discard subnormal bit + } // else +/-0 -> +/-0 + return Float.intBitsToFloat( // combine all parts + (hbits & 0x8000) << 16 // sign << ( 31 - 15 ) + | (exp | mant) << 13); // value << ( 23 - 10 ) } - if (val >= 0x38800000) // remains normalized value - return sign | val - 0x38000000 >>> 13; // exp - 127 + 15 - if (val < 0x33000000) // too small for subnormal - return sign; // becomes +/-0 - val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc - return sign | ((fbits & 0x7fffff | 0x800000) // add subnormal bit - + (0x800000 >>> val - 102) // round depending on cut off - >>> 126 - val); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0 - } + /** + * Calculate a 16 bit representation of a floating point value. The upper 16 bits of the result + * are 0. + * + * + * @param fval + * @return + */ + // returns all higher 16 bits as 0 for all results + public static int fromFloat(float fval) { + int fbits = Float.floatToIntBits(fval); + int sign = fbits >>> 16 & 0x8000; // sign only + int val = (fbits & 0x7fffffff) + 0x1000; // rounded value + + if (val >= 0x47800000) // might be or become NaN/Inf + { // avoid Inf due to rounding + if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf + if (val < 0x7f800000) // was value but too large + return sign | 0x7c00; // make it +/-Inf + return sign + | 0x7c00 + | // remains +/-Inf or NaN + (fbits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits + } + return sign | 0x7bff; // unrounded not quite Inf + } + if (val >= 0x38800000) // remains normalized value + return sign | val - 0x38000000 >>> 13; // exp - 127 + 15 + if (val < 0x33000000) // too small for subnormal + return sign; // becomes +/-0 + val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc + return sign + | ((fbits & 0x7fffff | 0x800000) // add subnormal bit + + (0x800000 >>> val - 102) // round depending on cut off + >>> 126 - val); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0 + } } diff --git a/src/main/java/terrasaur/utils/BinaryUtils.java b/src/main/java/terrasaur/utils/BinaryUtils.java index 4af5f3f..2571525 100644 --- a/src/main/java/terrasaur/utils/BinaryUtils.java +++ b/src/main/java/terrasaur/utils/BinaryUtils.java @@ -28,85 +28,83 @@ import java.io.IOException; /** * Static methods to work with bits and bytes - * + * * @author Hari.Nair@jhuapl.edu * */ public class BinaryUtils { - static public float readFloatAndSwap(DataInputStream is) throws IOException { - int intValue = is.readInt(); - intValue = ByteSwapper.swap(intValue); - return Float.intBitsToFloat(intValue); - } + public static float readFloatAndSwap(DataInputStream is) throws IOException { + int intValue = is.readInt(); + intValue = ByteSwapper.swap(intValue); + return Float.intBitsToFloat(intValue); + } - static public double readDoubleAndSwap(DataInputStream is) throws IOException { - long longValue = is.readLong(); - longValue = ByteSwapper.swap(longValue); - return Double.longBitsToDouble(longValue); - } + public static double readDoubleAndSwap(DataInputStream is) throws IOException { + long longValue = is.readLong(); + longValue = ByteSwapper.swap(longValue); + return Double.longBitsToDouble(longValue); + } - static public void writeFloatAndSwap(DataOutputStream os, float value) throws IOException { - int intValue = Float.floatToRawIntBits(value); - intValue = ByteSwapper.swap(intValue); - os.writeInt(intValue); - } + public static void writeFloatAndSwap(DataOutputStream os, float value) throws IOException { + int intValue = Float.floatToRawIntBits(value); + intValue = ByteSwapper.swap(intValue); + os.writeInt(intValue); + } - static public void writeDoubleAndSwap(DataOutputStream os, double value) throws IOException { - long longValue = Double.doubleToRawLongBits(value); - longValue = ByteSwapper.swap(longValue); - os.writeLong(longValue); - } + public static void writeDoubleAndSwap(DataOutputStream os, double value) throws IOException { + long longValue = Double.doubleToRawLongBits(value); + longValue = ByteSwapper.swap(longValue); + os.writeLong(longValue); + } + // This function is taken from + // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm + public static short swap(short value) { + int b1 = value & 0xff; + int b2 = (value >> 8) & 0xff; - // This function is taken from - // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm - static public short swap(short value) { - int b1 = value & 0xff; - int b2 = (value >> 8) & 0xff; + return (short) (b1 << 8 | b2 << 0); + } - return (short) (b1 << 8 | b2 << 0); - } + // This function is taken from + // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm + public static int swap(int value) { + int b1 = (value >> 0) & 0xff; + int b2 = (value >> 8) & 0xff; + int b3 = (value >> 16) & 0xff; + int b4 = (value >> 24) & 0xff; - // This function is taken from - // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm - static public int swap(int value) { - int b1 = (value >> 0) & 0xff; - int b2 = (value >> 8) & 0xff; - int b3 = (value >> 16) & 0xff; - int b4 = (value >> 24) & 0xff; + return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; + } - return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; - } + // This function is taken from + // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm + public static long swap(long value) { + long b1 = (value >> 0) & 0xff; + long b2 = (value >> 8) & 0xff; + long b3 = (value >> 16) & 0xff; + long b4 = (value >> 24) & 0xff; + long b5 = (value >> 32) & 0xff; + long b6 = (value >> 40) & 0xff; + long b7 = (value >> 48) & 0xff; + long b8 = (value >> 56) & 0xff; - // This function is taken from - // http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm - public static long swap(long value) { - long b1 = (value >> 0) & 0xff; - long b2 = (value >> 8) & 0xff; - long b3 = (value >> 16) & 0xff; - long b4 = (value >> 24) & 0xff; - long b5 = (value >> 32) & 0xff; - long b6 = (value >> 40) & 0xff; - long b7 = (value >> 48) & 0xff; - long b8 = (value >> 56) & 0xff; - - return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0; - } - - /** - * Byte swap a single float value. This function is taken from - * http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm - * This method should NOT be used to read little endian data! Instead, use - * LittleEndianDataInputStream! - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static float swap(float value) { - int intValue = Float.floatToIntBits(value); - intValue = swap(intValue); - return Float.intBitsToFloat(intValue); - } + return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0; + } + /** + * Byte swap a single float value. This function is taken from + * http://www.java2s.com/Code/Java/Language-Basics/Utilityforbyteswappingofalljavadatatypes.htm + * This method should NOT be used to read little endian data! Instead, use + * LittleEndianDataInputStream! + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static float swap(float value) { + int intValue = Float.floatToIntBits(value); + intValue = swap(intValue); + return Float.intBitsToFloat(intValue); + } } diff --git a/src/main/java/terrasaur/utils/ByteSwapper.java b/src/main/java/terrasaur/utils/ByteSwapper.java index 895a4af..322666b 100644 --- a/src/main/java/terrasaur/utils/ByteSwapper.java +++ b/src/main/java/terrasaur/utils/ByteSwapper.java @@ -24,7 +24,7 @@ package terrasaur.utils; /* * (C) 2004 - Geotechnical Software Services - * + * * This code is free software; you can redistribute it and/or modify it under the terms of the GNU * Lesser General Public License as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. @@ -40,153 +40,127 @@ package terrasaur.utils; // package no.geosoft.cc.util; - - /** * Utility class for doing byte swapping (i.e. conversion between little-endian and big-endian * representations) of different data types. Byte swapping is typically used when data is read from * a stream delivered by a system of different endian type as the present one. - * + * * @author Jacob Dreyer */ public class ByteSwapper { - /** - * Byte swap a single short value. - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static short swap(short value) { - int b1 = value & 0xff; - int b2 = (value >> 8) & 0xff; + /** + * Byte swap a single short value. + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static short swap(short value) { + int b1 = value & 0xff; + int b2 = (value >> 8) & 0xff; - return (short) (b1 << 8 | b2 << 0); - } + return (short) (b1 << 8 | b2 << 0); + } + /** + * Byte swap a single int value. + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static int swap(int value) { + int b1 = (value >> 0) & 0xff; + int b2 = (value >> 8) & 0xff; + int b3 = (value >> 16) & 0xff; + int b4 = (value >> 24) & 0xff; + return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; + } - /** - * Byte swap a single int value. - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static int swap(int value) { - int b1 = (value >> 0) & 0xff; - int b2 = (value >> 8) & 0xff; - int b3 = (value >> 16) & 0xff; - int b4 = (value >> 24) & 0xff; + /** + * Byte swap a single long value. + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static long swap(long value) { + long b1 = (value >> 0) & 0xff; + long b2 = (value >> 8) & 0xff; + long b3 = (value >> 16) & 0xff; + long b4 = (value >> 24) & 0xff; + long b5 = (value >> 32) & 0xff; + long b6 = (value >> 40) & 0xff; + long b7 = (value >> 48) & 0xff; + long b8 = (value >> 56) & 0xff; - return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; - } + return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0; + } + /** + * Byte swap a single float value. + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static float swap(float value) { + int intValue = Float.floatToIntBits(value); + intValue = swap(intValue); + return Float.intBitsToFloat(intValue); + } + /** + * Byte swap a single double value. + * + * @param value Value to byte swap. + * @return Byte swapped representation. + */ + public static double swap(double value) { + long longValue = Double.doubleToLongBits(value); + longValue = swap(longValue); + return Double.longBitsToDouble(longValue); + } - /** - * Byte swap a single long value. - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static long swap(long value) { - long b1 = (value >> 0) & 0xff; - long b2 = (value >> 8) & 0xff; - long b3 = (value >> 16) & 0xff; - long b4 = (value >> 24) & 0xff; - long b5 = (value >> 32) & 0xff; - long b6 = (value >> 40) & 0xff; - long b7 = (value >> 48) & 0xff; - long b8 = (value >> 56) & 0xff; + /** + * Byte swap an array of shorts. The result of the swapping is put back into the specified array. + * + * @param array Array of values to swap + */ + public static void swap(short[] array) { + for (int i = 0; i < array.length; i++) array[i] = swap(array[i]); + } - return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0; - } + /** + * Byte swap an array of ints. The result of the swapping is put back into the specified array. + * + * @param array Array of values to swap + */ + public static void swap(int[] array) { + for (int i = 0; i < array.length; i++) array[i] = swap(array[i]); + } + /** + * Byte swap an array of longs. The result of the swapping is put back into the specified array. + * + * @param array Array of values to swap + */ + public static void swap(long[] array) { + for (int i = 0; i < array.length; i++) array[i] = swap(array[i]); + } + /** + * Byte swap an array of floats. The result of the swapping is put back into the specified array. + * + * @param array Array of values to swap + */ + public static void swap(float[] array) { + for (int i = 0; i < array.length; i++) array[i] = swap(array[i]); + } - /** - * Byte swap a single float value. - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static float swap(float value) { - int intValue = Float.floatToIntBits(value); - intValue = swap(intValue); - return Float.intBitsToFloat(intValue); - } - - - - /** - * Byte swap a single double value. - * - * @param value Value to byte swap. - * @return Byte swapped representation. - */ - public static double swap(double value) { - long longValue = Double.doubleToLongBits(value); - longValue = swap(longValue); - return Double.longBitsToDouble(longValue); - } - - - - /** - * Byte swap an array of shorts. The result of the swapping is put back into the specified array. - * - * @param array Array of values to swap - */ - public static void swap(short[] array) { - for (int i = 0; i < array.length; i++) - array[i] = swap(array[i]); - } - - - - /** - * Byte swap an array of ints. The result of the swapping is put back into the specified array. - * - * @param array Array of values to swap - */ - public static void swap(int[] array) { - for (int i = 0; i < array.length; i++) - array[i] = swap(array[i]); - } - - - - /** - * Byte swap an array of longs. The result of the swapping is put back into the specified array. - * - * @param array Array of values to swap - */ - public static void swap(long[] array) { - for (int i = 0; i < array.length; i++) - array[i] = swap(array[i]); - } - - - - /** - * Byte swap an array of floats. The result of the swapping is put back into the specified array. - * - * @param array Array of values to swap - */ - public static void swap(float[] array) { - for (int i = 0; i < array.length; i++) - array[i] = swap(array[i]); - } - - - - /** - * Byte swap an array of doubles. The result of the swapping is put back into the specified array. - * - * @param array Array of values to swap - */ - public static void swap(double[] array) { - for (int i = 0; i < array.length; i++) - array[i] = swap(array[i]); - } + /** + * Byte swap an array of doubles. The result of the swapping is put back into the specified array. + * + * @param array Array of values to swap + */ + public static void swap(double[] array) { + for (int i = 0; i < array.length; i++) array[i] = swap(array[i]); + } } - diff --git a/src/main/java/terrasaur/utils/CellInfo.java b/src/main/java/terrasaur/utils/CellInfo.java index 19818be..317b27c 100644 --- a/src/main/java/terrasaur/utils/CellInfo.java +++ b/src/main/java/terrasaur/utils/CellInfo.java @@ -44,294 +44,286 @@ import vtk.vtkPolyData; * associated with ALTWG processing. Also contains static methods for dealing with cell data. The * attributes are specific to ALTWG classes which precludes this class from being generic enough to * reside in saavtk. - * + * * @author espirrc1 * */ @Value.Immutable public abstract class CellInfo { - private final static Logger logger = LogManager.getLogger(CellInfo.class); + private static final Logger logger = LogManager.getLogger(CellInfo.class); - abstract Vector3D pt0(); + abstract Vector3D pt0(); - abstract Vector3D pt1(); + abstract Vector3D pt1(); - abstract Vector3D pt2(); + abstract Vector3D pt2(); - /** - * returns the unitized cross product of (v3-v2)x(v1-v2). Vertices are in counterclockwise order. - */ - public abstract Vector3D normal(); + /** + * returns the unitized cross product of (v3-v2)x(v1-v2). Vertices are in counterclockwise order. + */ + public abstract Vector3D normal(); - public abstract Vector3D center(); + public abstract Vector3D center(); - abstract double area(); + abstract double area(); - /** latitude in degrees */ - abstract double latitude(); + /** latitude in degrees */ + abstract double latitude(); - /** longitude in degrees */ - abstract double longitude(); + /** longitude in degrees */ + abstract double longitude(); - abstract double radius(); + abstract double radius(); - /** angle between normal and radial, or its supplement if they are more than 90 degrees apart */ - abstract double tiltDeg(); + /** angle between normal and radial, or its supplement if they are more than 90 degrees apart */ + abstract double tiltDeg(); - /** - * Basic tilt direction definition: Project plate normal into the plate facet. Determine angle in - * CW of projected facet assuming up or N is 0deg. Take longitude and generate a quaternion - * representing rotation about the Z-axis. Rotate normal vector by quaternion such that new - * coordinate system x' is now along R. Call this n'. - * - * Tilt direction B = 90 - atan2(n'[3],n'[2]) if B < 0 then B = 360 + B - */ - abstract double tiltDirDeg(); + /** + * Basic tilt direction definition: Project plate normal into the plate facet. Determine angle in + * CW of projected facet assuming up or N is 0deg. Take longitude and generate a quaternion + * representing rotation about the Z-axis. Rotate normal vector by quaternion such that new + * coordinate system x' is now along R. Call this n'. + * + * Tilt direction B = 90 - atan2(n'[3],n'[2]) if B < 0 then B = 360 + B + */ + abstract double tiltDirDeg(); - /** - * Radius of a circle enclosing an equilateral triangle with an area equal to this facet's area. - */ - abstract double circumscribingRadius(); + /** + * Radius of a circle enclosing an equilateral triangle with an area equal to this facet's area. + */ + abstract double circumscribingRadius(); - private final static HashMap> completeMap = - new HashMap<>(); + private static final HashMap> completeMap = new HashMap<>(); - public static synchronized void removeKey(vtkPolyData polydata) { - completeMap.remove(polydata); - } - - /** - * Find the cell with index cellId. - * - * @param polydata - * @param cellId - * @param idList will contain vertex indices on return - * @param pt0 will contain first vertex on return - * @param pt1 will contain second vertex on return - * @param pt2 will contain third vertex on return - */ - public static void getCellPoints(vtkPolyData polydata, long cellId, vtkIdList idList, double[] pt0, - double[] pt1, double[] pt2) { - polydata.GetCellPoints(cellId, idList); - - long numberOfCells = idList.GetNumberOfIds(); - if (numberOfCells != 3) { - logger.warn("CellInfo.getCellPoints(): cell {} must have 3 vertices but {} in list", cellId, - idList.GetNumberOfIds()); - for (int i = 0; i < idList.GetNumberOfIds(); i++) - logger.warn("cellIdList[{}] = {}", i, idList.GetId(i)); - return; + public static synchronized void removeKey(vtkPolyData polydata) { + completeMap.remove(polydata); } - polydata.GetPoint(idList.GetId(0), pt0); - polydata.GetPoint(idList.GetId(1), pt1); - polydata.GetPoint(idList.GetId(2), pt2); - } + /** + * Find the cell with index cellId. + * + * @param polydata + * @param cellId + * @param idList will contain vertex indices on return + * @param pt0 will contain first vertex on return + * @param pt1 will contain second vertex on return + * @param pt2 will contain third vertex on return + */ + public static void getCellPoints( + vtkPolyData polydata, long cellId, vtkIdList idList, double[] pt0, double[] pt1, double[] pt2) { + polydata.GetCellPoints(cellId, idList); - public static CellInfo fromPoints(double[] pt0, double[] pt1, double[] pt2) { + long numberOfCells = idList.GetNumberOfIds(); + if (numberOfCells != 3) { + logger.warn( + "CellInfo.getCellPoints(): cell {} must have 3 vertices but {} in list", + cellId, + idList.GetNumberOfIds()); + for (int i = 0; i < idList.GetNumberOfIds(); i++) logger.warn("cellIdList[{}] = {}", i, idList.GetId(i)); + return; + } - ImmutableCellInfo.Builder builder = ImmutableCellInfo.builder(); - builder.pt0(new Vector3D(pt0)); - builder.pt1(new Vector3D(pt1)); - builder.pt2(new Vector3D(pt2)); - - TriangularFacet tf = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - Vector3D normal = MathConversions.toVector3D(tf.getNormal()); - builder.normal(normal); - - Vector3D center = MathConversions.toVector3D(tf.getCenter()); - builder.center(center); - - double area = tf.getArea(); - builder.area(area); - builder.circumscribingRadius(Math.sqrt(4 / (3 * Math.sqrt(3)) * area)); - - double latitude = Math.toDegrees(center.getDelta()); - double longitude = Math.toDegrees(center.getAlpha()); - - if (longitude < 0) - longitude += 360; - - builder.latitude(latitude); - builder.longitude(longitude); - builder.radius(center.getNorm()); - - builder.tiltDeg(tiltDeg(center, normal)); - builder.tiltDirDeg(tiltDirDeg(longitude, normal)); - - return builder.build(); - } - - /** - * Return the angle between the radial and normal vectors in degrees. The angle is constrained to - * be between 0 and 90 degrees. - * - * @param radial - * @param normal - * @return - */ - public static double tiltDeg(double[] radial, double[] normal) { - return tiltDeg(new Vector3D(radial), new Vector3D(normal)); - } - - /** - * Return the angle between the radial and normal vectors in degrees. The angle is constrained to - * be between 0 and 90 degrees. - * - * @param radial - * @param normal - * @return - */ - public static double tiltDeg(Vector3D radial, Vector3D normal) { - double tiltDeg = Math.toDegrees(Vector3D.angle(radial, normal)); - if (tiltDeg > 90) - tiltDeg = 180 - tiltDeg; - return tiltDeg; - } - - public static double tiltDirDeg(double lonDeg, double[] normal) { - return tiltDirDeg(lonDeg, new Vector3D(normal)); - } - - public static double tiltDirDeg(double lonDeg, Vector3D normal) { - Rotation r = - new Rotation(Vector3D.PLUS_K, Math.toRadians(lonDeg), RotationConvention.FRAME_TRANSFORM); - Vector3D newVector = r.applyTo(normal); - - double atan2D = newVector.getAlpha(); - double tiltDirDeg = 90 - Math.toDegrees(atan2D); - if (tiltDirDeg < 0) - tiltDirDeg = 360D + tiltDirDeg; - return tiltDirDeg; - } - - /** - * Find the cell with index cellId. - * - * @param polydata - * @param cellId - * @param idList - * @return - */ - public static CellInfo getCellInfo(vtkPolyData polydata, long cellId, vtkIdList idList) { - return getCellInfo(polydata, cellId, idList, false); - } - - /** - * Find the cell with index cellId. - * - * @param polydata - * @param cellId - * @param idList - * @param CACHE_CELLINFO add this cell to the cached map - * @return - */ - public static synchronized CellInfo getCellInfo(vtkPolyData polydata, long cellId, - vtkIdList idList, boolean CACHE_CELLINFO) { - HashMap ciMap = completeMap.get(polydata); - if (ciMap == null) { - ciMap = new HashMap<>(); - if (CACHE_CELLINFO) - completeMap.put(polydata, ciMap); + polydata.GetPoint(idList.GetId(0), pt0); + polydata.GetPoint(idList.GetId(1), pt1); + polydata.GetPoint(idList.GetId(2), pt2); } - CellInfo ci = ciMap.get(cellId); - if (ci == null) { - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; - getCellPoints(polydata, cellId, idList, pt0, pt1, pt2); - ci = fromPoints(pt0, pt1, pt2); + public static CellInfo fromPoints(double[] pt0, double[] pt1, double[] pt2) { - if (CACHE_CELLINFO) - ciMap.put(cellId, ci); - } else { - // need to populate idList as it is not cached - polydata.GetCellPoints(cellId, idList); - } - return ci; - } + ImmutableCellInfo.Builder builder = ImmutableCellInfo.builder(); + builder.pt0(new Vector3D(pt0)); + builder.pt1(new Vector3D(pt1)); + builder.pt2(new Vector3D(pt2)); - /** - * Given a polydata model and vtkFloatArrays containing data at each cell in the polydata model, - * calculate the data values at points in the polydata model. - * - * @param dem - * @param cellData - * @param pointData - */ - public static void convertCellDataToPointData(vtkPolyData dem, - HashMap cellData, HashMap pointData) { + TriangularFacet tf = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + Vector3D normal = MathConversions.toVector3D(tf.getNormal()); + builder.normal(normal); - vtkCellDataToPointData cellToPoint = new vtkCellDataToPointData(); - cellToPoint.SetInputData(dem); + Vector3D center = MathConversions.toVector3D(tf.getCenter()); + builder.center(center); - for (String arrayName : cellData.keySet()) { - vtkFloatArray array = cellData.get(arrayName); - dem.GetCellData().SetScalars(array); - cellToPoint.Update(); - vtkFloatArray arrayPoint = new vtkFloatArray(); - vtkDataArray outputScalars = - ((vtkPolyData) cellToPoint.GetOutput()).GetPointData().GetScalars(); - arrayPoint.DeepCopy(outputScalars); - pointData.put(arrayName, arrayPoint); + double area = tf.getArea(); + builder.area(area); + builder.circumscribingRadius(Math.sqrt(4 / (3 * Math.sqrt(3)) * area)); + + double latitude = Math.toDegrees(center.getDelta()); + double longitude = Math.toDegrees(center.getAlpha()); + + if (longitude < 0) longitude += 360; + + builder.latitude(latitude); + builder.longitude(longitude); + builder.radius(center.getNorm()); + + builder.tiltDeg(tiltDeg(center, normal)); + builder.tiltDirDeg(tiltDirDeg(longitude, normal)); + + return builder.build(); } - dem.GetPointData().SetScalars(null); + /** + * Return the angle between the radial and normal vectors in degrees. The angle is constrained to + * be between 0 and 90 degrees. + * + * @param radial + * @param normal + * @return + */ + public static double tiltDeg(double[] radial, double[] normal) { + return tiltDeg(new Vector3D(radial), new Vector3D(normal)); + } - cellToPoint.Delete(); - } + /** + * Return the angle between the radial and normal vectors in degrees. The angle is constrained to + * be between 0 and 90 degrees. + * + * @param radial + * @param normal + * @return + */ + public static double tiltDeg(Vector3D radial, Vector3D normal) { + double tiltDeg = Math.toDegrees(Vector3D.angle(radial, normal)); + if (tiltDeg > 90) tiltDeg = 180 - tiltDeg; + return tiltDeg; + } - /** - * Converts a vtkFloat array of data values at the facet center into a vtkFloat array of data - * values at the vertices. Assumes input vtkFloatArray contains data that are in the same order as - * the cells in the dem, otherwise this doesn't work. - * - * @param dem - * @param cellData - * @return - */ - public static vtkFloatArray convertvtkCellToPoint(vtkPolyData dem, vtkFloatArray cellData) { - vtkCellDataToPointData cellToPoint = new vtkCellDataToPointData(); - cellToPoint.SetInputData(dem); + public static double tiltDirDeg(double lonDeg, double[] normal) { + return tiltDirDeg(lonDeg, new Vector3D(normal)); + } - dem.GetCellData().SetScalars(cellData); - cellToPoint.Update(); - vtkFloatArray pointData = new vtkFloatArray(); - vtkDataArray outputScalars = - ((vtkPolyData) cellToPoint.GetOutput()).GetPointData().GetScalars(); - pointData.DeepCopy(outputScalars); + public static double tiltDirDeg(double lonDeg, Vector3D normal) { + Rotation r = new Rotation(Vector3D.PLUS_K, Math.toRadians(lonDeg), RotationConvention.FRAME_TRANSFORM); + Vector3D newVector = r.applyTo(normal); - dem.GetPointData().SetScalars(null); - cellToPoint.Delete(); + double atan2D = newVector.getAlpha(); + double tiltDirDeg = 90 - Math.toDegrees(atan2D); + if (tiltDirDeg < 0) tiltDirDeg = 360D + tiltDirDeg; + return tiltDirDeg; + } - return pointData; - } + /** + * Find the cell with index cellId. + * + * @param polydata + * @param cellId + * @param idList + * @return + */ + public static CellInfo getCellInfo(vtkPolyData polydata, long cellId, vtkIdList idList) { + return getCellInfo(polydata, cellId, idList, false); + } - /** - * Converts a vtkFloat array of data values at the vertices into a vtkFloat array of data values - * at the facet centers. Assumes vertex data are in the same order as the vertices for each facet - * in the dem, otherwise this doesn't work. - * - * @param dem - * @param pointData - * @return - */ - public static vtkFloatArray convertvtkPointToCell(vtkPolyData dem, vtkFloatArray pointData) { - vtkPointDataToCellData pointToCell = new vtkPointDataToCellData(); - pointToCell.SetInputData(dem); - dem.GetPointData().SetScalars(pointData); - pointToCell.Update(); - vtkFloatArray arrayCell = new vtkFloatArray(); - vtkDataArray outputScalars = ((vtkPolyData) pointToCell.GetOutput()).GetCellData().GetScalars(); - arrayCell.DeepCopy(outputScalars); - dem.GetPointData().SetScalars(null); - pointToCell.Delete(); + /** + * Find the cell with index cellId. + * + * @param polydata + * @param cellId + * @param idList + * @param CACHE_CELLINFO add this cell to the cached map + * @return + */ + public static synchronized CellInfo getCellInfo( + vtkPolyData polydata, long cellId, vtkIdList idList, boolean CACHE_CELLINFO) { + HashMap ciMap = completeMap.get(polydata); + if (ciMap == null) { + ciMap = new HashMap<>(); + if (CACHE_CELLINFO) completeMap.put(polydata, ciMap); + } - return arrayCell; + CellInfo ci = ciMap.get(cellId); + if (ci == null) { + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; + getCellPoints(polydata, cellId, idList, pt0, pt1, pt2); + ci = fromPoints(pt0, pt1, pt2); - } + if (CACHE_CELLINFO) ciMap.put(cellId, ci); + } else { + // need to populate idList as it is not cached + polydata.GetCellPoints(cellId, idList); + } + return ci; + } + /** + * Given a polydata model and vtkFloatArrays containing data at each cell in the polydata model, + * calculate the data values at points in the polydata model. + * + * @param dem + * @param cellData + * @param pointData + */ + public static void convertCellDataToPointData( + vtkPolyData dem, HashMap cellData, HashMap pointData) { + + vtkCellDataToPointData cellToPoint = new vtkCellDataToPointData(); + cellToPoint.SetInputData(dem); + + for (String arrayName : cellData.keySet()) { + vtkFloatArray array = cellData.get(arrayName); + dem.GetCellData().SetScalars(array); + cellToPoint.Update(); + vtkFloatArray arrayPoint = new vtkFloatArray(); + vtkDataArray outputScalars = + ((vtkPolyData) cellToPoint.GetOutput()).GetPointData().GetScalars(); + arrayPoint.DeepCopy(outputScalars); + pointData.put(arrayName, arrayPoint); + } + + dem.GetPointData().SetScalars(null); + + cellToPoint.Delete(); + } + + /** + * Converts a vtkFloat array of data values at the facet center into a vtkFloat array of data + * values at the vertices. Assumes input vtkFloatArray contains data that are in the same order as + * the cells in the dem, otherwise this doesn't work. + * + * @param dem + * @param cellData + * @return + */ + public static vtkFloatArray convertvtkCellToPoint(vtkPolyData dem, vtkFloatArray cellData) { + vtkCellDataToPointData cellToPoint = new vtkCellDataToPointData(); + cellToPoint.SetInputData(dem); + + dem.GetCellData().SetScalars(cellData); + cellToPoint.Update(); + vtkFloatArray pointData = new vtkFloatArray(); + vtkDataArray outputScalars = + ((vtkPolyData) cellToPoint.GetOutput()).GetPointData().GetScalars(); + pointData.DeepCopy(outputScalars); + + dem.GetPointData().SetScalars(null); + cellToPoint.Delete(); + + return pointData; + } + + /** + * Converts a vtkFloat array of data values at the vertices into a vtkFloat array of data values + * at the facet centers. Assumes vertex data are in the same order as the vertices for each facet + * in the dem, otherwise this doesn't work. + * + * @param dem + * @param pointData + * @return + */ + public static vtkFloatArray convertvtkPointToCell(vtkPolyData dem, vtkFloatArray pointData) { + vtkPointDataToCellData pointToCell = new vtkPointDataToCellData(); + pointToCell.SetInputData(dem); + dem.GetPointData().SetScalars(pointData); + pointToCell.Update(); + vtkFloatArray arrayCell = new vtkFloatArray(); + vtkDataArray outputScalars = + ((vtkPolyData) pointToCell.GetOutput()).GetCellData().GetScalars(); + arrayCell.DeepCopy(outputScalars); + dem.GetPointData().SetScalars(null); + pointToCell.Delete(); + + return arrayCell; + } } diff --git a/src/main/java/terrasaur/utils/DTMHeader.java b/src/main/java/terrasaur/utils/DTMHeader.java index e62468b..0eda45f 100644 --- a/src/main/java/terrasaur/utils/DTMHeader.java +++ b/src/main/java/terrasaur/utils/DTMHeader.java @@ -29,8 +29,7 @@ import terrasaur.fits.FitsData; public interface DTMHeader { - public List createFitsHeader(List planeList) throws HeaderCardException; - - public void setData(FitsData fitsData); + public List createFitsHeader(List planeList) throws HeaderCardException; + public void setData(FitsData fitsData); } diff --git a/src/main/java/terrasaur/utils/FitPlane.java b/src/main/java/terrasaur/utils/FitPlane.java index d594913..faf2db6 100644 --- a/src/main/java/terrasaur/utils/FitPlane.java +++ b/src/main/java/terrasaur/utils/FitPlane.java @@ -35,127 +35,122 @@ import terrasaur.utils.math.RotationUtils; /** * Transform points in a global coordinate system to a local one. - * + * * @author Hari.Nair@jhuapl.edu * */ public class FitPlane { - private Rotation rotation; - private Vector3D translation; + private Rotation rotation; + private Vector3D translation; - private FitPlane() { + private FitPlane() {} - } + public FitPlane(List points) { + Vector3D center = new Vector3D(0, 0, 0); + for (Vector3D p : points) center = center.add(p); + center = center.scalarMultiply(1. / points.size()); - public FitPlane(List points) { - Vector3D center = new Vector3D(0, 0, 0); - for (Vector3D p : points) - center = center.add(p); - center = center.scalarMultiply(1. / points.size()); + double[][] array = new double[3][points.size()]; + for (int i = 0; i < points.size(); i++) { + Vector3D p = points.get(i); + array[0][i] = p.getX() - center.getX(); + array[1][i] = p.getY() - center.getY(); + array[2][i] = p.getZ() - center.getZ(); + } - double[][] array = new double[3][points.size()]; - for (int i = 0; i < points.size(); i++) { - Vector3D p = points.get(i); - array[0][i] = p.getX() - center.getX(); - array[1][i] = p.getY() - center.getY(); - array[2][i] = p.getZ() - center.getZ(); + RealMatrix pointMatrix = new Array2DRowRealMatrix(array, false); + SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); + RealMatrix u = svd.getU(); + + Vector3D zAxis = new Vector3D(u.getColumn(2)).normalize(); + if (zAxis.dotProduct(center) < 0) zAxis = zAxis.negate(); + + Vector3D xAxis = new Vector3D(u.getColumn(0)).normalize(); + + rotation = RotationUtils.KprimaryIsecondary(zAxis, xAxis); + translation = center; } - RealMatrix pointMatrix = new Array2DRowRealMatrix(array, false); - SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); - RealMatrix u = svd.getU(); - - Vector3D zAxis = new Vector3D(u.getColumn(2)).normalize(); - if (zAxis.dotProduct(center) < 0) - zAxis = zAxis.negate(); - - Vector3D xAxis = new Vector3D(u.getColumn(0)).normalize(); - - rotation = RotationUtils.KprimaryIsecondary(zAxis, xAxis); - translation = center; - } - - /** - * Get the transform to convert points in global coordinates to local coordinates.
- * - *
-   * Pair<RotationMatrixIJK, VectorIJK> p = getTransform();
-   * VectorIJK local = p.getKey().mxv(VectorIJK.subtract(global, p.getValue()))
-   * 
-   *  
- * - * @return - */ - public Pair getTransform() { - return new Pair(rotation, translation); - } - - /** - * Transform a vector in the global coordinate system to the local coordinate system - * - * @param global - * @return - */ - public Vector3D globalToLocal(Vector3D global) { - return globalToLocal(Arrays.asList(global)).get(0); - } - - /** - * Transform a {@link List} of vectors in the global coordinate system to the local coordinate - * system - * - * @param global - * @return - */ - public List globalToLocal(List global) { - Pair p = getTransform(); - List local = new ArrayList<>(); - for (Vector3D v : global) { - local.add(p.getKey().applyTo(v.subtract(p.getValue()))); + /** + * Get the transform to convert points in global coordinates to local coordinates.
+ * + *
+     * Pair<RotationMatrixIJK, VectorIJK> p = getTransform();
+     * VectorIJK local = p.getKey().mxv(VectorIJK.subtract(global, p.getValue()))
+     * 
+     *  
+ * + * @return + */ + public Pair getTransform() { + return new Pair(rotation, translation); } - return local; - } - /** - * Transform a vector in the global coordinate system to the local coordinate system - * - * @param local - * @return - */ - public Vector3D localToGlobal(Vector3D local) { - return localToGlobal(Arrays.asList(local)).get(0); - } - - /** - * Transform a vector in the global coordinate system to the local coordinate system - * - * @param local - * @return - */ - public List localToGlobal(List local) { - Pair p = getTransform(); - List global = new ArrayList<>(); - for (Vector3D v : local) { - global.add(p.getKey().applyInverseTo(v).add(p.getValue())); + /** + * Transform a vector in the global coordinate system to the local coordinate system + * + * @param global + * @return + */ + public Vector3D globalToLocal(Vector3D global) { + return globalToLocal(Arrays.asList(global)).get(0); } - return global; - } - /** - * Return a plane with the normal pointing in the opposite direction - * - * @return - */ - public FitPlane reverseNormal() { - FitPlane fp = new FitPlane(); - Vector3D zAxis = rotation.applyInverseTo(Vector3D.PLUS_K).negate(); - Vector3D xAxis = rotation.applyInverseTo(Vector3D.PLUS_I); + /** + * Transform a {@link List} of vectors in the global coordinate system to the local coordinate + * system + * + * @param global + * @return + */ + public List globalToLocal(List global) { + Pair p = getTransform(); + List local = new ArrayList<>(); + for (Vector3D v : global) { + local.add(p.getKey().applyTo(v.subtract(p.getValue()))); + } + return local; + } - fp.translation = translation; - fp.rotation = RotationUtils.KprimaryIsecondary(zAxis, xAxis); + /** + * Transform a vector in the global coordinate system to the local coordinate system + * + * @param local + * @return + */ + public Vector3D localToGlobal(Vector3D local) { + return localToGlobal(Arrays.asList(local)).get(0); + } - return fp; - } + /** + * Transform a vector in the global coordinate system to the local coordinate system + * + * @param local + * @return + */ + public List localToGlobal(List local) { + Pair p = getTransform(); + List global = new ArrayList<>(); + for (Vector3D v : local) { + global.add(p.getKey().applyInverseTo(v).add(p.getValue())); + } + return global; + } + /** + * Return a plane with the normal pointing in the opposite direction + * + * @return + */ + public FitPlane reverseNormal() { + FitPlane fp = new FitPlane(); + Vector3D zAxis = rotation.applyInverseTo(Vector3D.PLUS_K).negate(); + Vector3D xAxis = rotation.applyInverseTo(Vector3D.PLUS_I); + + fp.translation = translation; + fp.rotation = RotationUtils.KprimaryIsecondary(zAxis, xAxis); + + return fp; + } } diff --git a/src/main/java/terrasaur/utils/FitSurface.java b/src/main/java/terrasaur/utils/FitSurface.java index 1d89e1b..346e3c4 100644 --- a/src/main/java/terrasaur/utils/FitSurface.java +++ b/src/main/java/terrasaur/utils/FitSurface.java @@ -26,120 +26,116 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import net.jafama.FastMath; import org.apache.commons.math3.analysis.MultivariateFunction; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import org.apache.commons.math3.linear.MatrixUtils; import org.apache.commons.math3.linear.RealMatrix; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import net.jafama.FastMath; /** * Based on IDL's sfit routine. Fit a 2D polynomial to an input set of points. The fitting function * is
* f(x,y) = Σki,jxiyj
- * + * * Input points are assumed to be in a local coordinate system where X and Y are the coordinates in * the reference plane and Z is the height above the plane. This may require transforming the * original set of points to a local coordinate system with {@link FitPlane} before using this * class. - * + * * @author Hari.Nair@jhuapl.edu * */ public class FitSurface implements MultivariateFunction { - private final static Logger logger = LogManager.getLogger(FitSurface.class); + private static final Logger logger = LogManager.getLogger(FitSurface.class); - private List points; - private int degree; - private double[][] coefficients; + private List points; + private int degree; + private double[][] coefficients; - /** - * - * @param points points to fit - * @param degree degree of fitting polynomial (i.e. degree 3 means highest term in the polynomial - * is x3y3) - */ - public FitSurface(List points, int degree) { - this.points = points; - this.degree = degree; - fit(); - } - - /** - * - * @param x in local coordinates - * @param y in local coordinates - * @return function value - */ - public double value(double x, double y) { - List terms = new ArrayList<>(); - for (int i = 0; i <= degree; i++) { - double xi = FastMath.pow(x, i); - for (int j = 0; j <= degree; j++) { - double yj = FastMath.pow(y, j); - terms.add(coefficients[i][j] * xi * yj); - } + /** + * + * @param points points to fit + * @param degree degree of fitting polynomial (i.e. degree 3 means highest term in the polynomial + * is x3y3) + */ + public FitSurface(List points, int degree) { + this.points = points; + this.degree = degree; + fit(); } - Collections.sort(terms, new Comparator() { - @Override - public int compare(Double o1, Double o2) { - return Double.compare(Math.abs(o1), Math.abs(o2)); - } - }); - double f = 0; - for (Double term : terms) - f += term; - return f; - } - - @Override - public double value(double[] arg0) { - return value(arg0[0], arg0[1]); - } - - private void fit() { - int n2 = (degree + 1) * (degree + 1); - - RealMatrix x = MatrixUtils.createRealMatrix(points.size(), 1); - RealMatrix y = MatrixUtils.createRealMatrix(points.size(), 1); - RealMatrix z = MatrixUtils.createRealMatrix(points.size(), 1); - - for (int i = 0; i < points.size(); i++) { - Vector3D v = points.get(i); - x.setEntry(i, 0, v.getX()); - y.setEntry(i, 0, v.getY()); - z.setEntry(i, 0, v.getZ()); - } - - if (points.size() < n2) { - logger.warn("{} points supplied, need at least {} for fitting degree {}", points.size(), n2, - degree); - } - - RealMatrix ut = MatrixUtils.createRealMatrix(points.size(), n2); - - for (int ip = 0; ip < points.size(); ip++) { - for (int i = 0; i <= degree; i++) { - double xi = Math.pow(x.getEntry(ip, 0), i); - for (int j = 0; j <= degree; j++) { - double yj = Math.pow(y.getEntry(ip, 0), j); - ut.setEntry(ip, i * (degree + 1) + j, xi * yj); + /** + * + * @param x in local coordinates + * @param y in local coordinates + * @return function value + */ + public double value(double x, double y) { + List terms = new ArrayList<>(); + for (int i = 0; i <= degree; i++) { + double xi = FastMath.pow(x, i); + for (int j = 0; j <= degree; j++) { + double yj = FastMath.pow(y, j); + terms.add(coefficients[i][j] * xi * yj); + } } - } + + Collections.sort(terms, new Comparator() { + @Override + public int compare(Double o1, Double o2) { + return Double.compare(Math.abs(o1), Math.abs(o2)); + } + }); + double f = 0; + for (Double term : terms) f += term; + return f; } - RealMatrix kk = ut.multiply(MatrixUtils.inverse(ut.transpose().multiply(ut))); - RealMatrix kx1 = kk.transpose().multiply(z); - coefficients = new double[degree + 1][degree + 1]; - for (int i = 0; i <= degree; i++) { - for (int j = 0; j <= degree; j++) { - coefficients[i][j] = kx1.getEntry(i * (degree + 1) + j, 0); - } + @Override + public double value(double[] arg0) { + return value(arg0[0], arg0[1]); } - } + private void fit() { + int n2 = (degree + 1) * (degree + 1); + RealMatrix x = MatrixUtils.createRealMatrix(points.size(), 1); + RealMatrix y = MatrixUtils.createRealMatrix(points.size(), 1); + RealMatrix z = MatrixUtils.createRealMatrix(points.size(), 1); + + for (int i = 0; i < points.size(); i++) { + Vector3D v = points.get(i); + x.setEntry(i, 0, v.getX()); + y.setEntry(i, 0, v.getY()); + z.setEntry(i, 0, v.getZ()); + } + + if (points.size() < n2) { + logger.warn("{} points supplied, need at least {} for fitting degree {}", points.size(), n2, degree); + } + + RealMatrix ut = MatrixUtils.createRealMatrix(points.size(), n2); + + for (int ip = 0; ip < points.size(); ip++) { + for (int i = 0; i <= degree; i++) { + double xi = Math.pow(x.getEntry(ip, 0), i); + for (int j = 0; j <= degree; j++) { + double yj = Math.pow(y.getEntry(ip, 0), j); + ut.setEntry(ip, i * (degree + 1) + j, xi * yj); + } + } + } + + RealMatrix kk = ut.multiply(MatrixUtils.inverse(ut.transpose().multiply(ut))); + RealMatrix kx1 = kk.transpose().multiply(z); + coefficients = new double[degree + 1][degree + 1]; + for (int i = 0; i <= degree; i++) { + for (int j = 0; j <= degree; j++) { + coefficients[i][j] = kx1.getEntry(i * (degree + 1) + j, 0); + } + } + } } diff --git a/src/main/java/terrasaur/utils/GMTGridUtil.java b/src/main/java/terrasaur/utils/GMTGridUtil.java index 21a5511..e1db1a7 100644 --- a/src/main/java/terrasaur/utils/GMTGridUtil.java +++ b/src/main/java/terrasaur/utils/GMTGridUtil.java @@ -32,6 +32,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import nom.tam.fits.FitsException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.math3.analysis.interpolation.PiecewiseBicubicSplineInterpolatingFunction; import org.apache.commons.math3.analysis.interpolation.PiecewiseBicubicSplineInterpolator; @@ -41,634 +42,628 @@ import org.apache.commons.math3.linear.RealMatrix; import org.apache.commons.math3.linear.SingularValueDecomposition; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.commons.math3.util.Pair; -import nom.tam.fits.FitsException; -import terrasaur.fits.FitsUtil; import spice.basic.LatitudinalCoordinates; import spice.basic.Matrix33; import spice.basic.SpiceException; import spice.basic.Vector3; +import terrasaur.fits.FitsUtil; /** * This class takes a 3D field as input, uses GMT to create a uniform grid on a local plane, and * then returns the field values at these uniform grid points. - * + * * @author nairah1 * */ public class GMTGridUtil { - private List pointsList; - private List field; - private Matrix33 rotation; - private Vector3 translation; - private Vector3 uz; - private int halfSize; - private int nX, nY; - private double groundSampleDistance; - private String additionalGMTArgs; - private List globalXYZ; - private List evaluateXYZ; - private boolean evaluateAtCustomPoints; + private List pointsList; + private List field; + private Matrix33 rotation; + private Vector3 translation; + private Vector3 uz; + private int halfSize; + private int nX, nY; + private double groundSampleDistance; + private String additionalGMTArgs; + private List globalXYZ; + private List evaluateXYZ; + private boolean evaluateAtCustomPoints; - /** - * Specify the dimensions and grid spacing of the 2D grid used by GMT. nX and nY must be odd and - * equal. - * - * @param nX - * @param nY - * @param groundSampleDistance - */ - public GMTGridUtil(int nX, int nY, double groundSampleDistance) { - this.nX = nX; - this.nY = nY; - this.halfSize = (nX - 1) / 2; - checkDimensions(); - this.groundSampleDistance = groundSampleDistance; - this.evaluateAtCustomPoints = false; - this.additionalGMTArgs = ""; - } - - /** - * Specify the dimensions and grid spacing of the 2D grid used by GMT. - * - * @param halfSize grid dimensions are (2*halfSize +1)x(2*halfSize +1) - * @param groundSampleDistance - */ - public GMTGridUtil(int halfSize, double groundSampleDistance) { - this.halfSize = halfSize; - this.nX = halfSize * 2 + 1; - this.nY = halfSize * 2 + 1; - this.groundSampleDistance = groundSampleDistance; - this.evaluateAtCustomPoints = false; - this.additionalGMTArgs = ""; - } - - private void checkDimensions() { - if (nX != nY) { - throw new IllegalArgumentException( - String.format("GMTGridUtil: nX (%d) and nY (%d) must be equal!\n", nX, nY)); - } - if (nX % 2 != 1) { - throw new IllegalArgumentException(String.format("GMTGridUtil: nX (%d) must be odd!\n", nX)); - } - } - - public void setGMTArgs(String args) { - additionalGMTArgs = args; - } - - /** - * Specify the 3D coordinates for the input field - * - * @param x - * @param y - * @param z - */ - public void setXYZ(double[] x, double[] y, double[] z) { - pointsList = new ArrayList<>(); - for (int i = 0; i < x.length; i++) { - pointsList.add(new Vector3(x[i], y[i], z[i])); - } - - translation = new Vector3(); - for (Vector3 point : pointsList) - translation = translation.add(point); - translation = translation.scale(1. / pointsList.size()); - - try { - calculateTransformation(); - } catch (SpiceException e) { - e.printStackTrace(); - System.exit(0); - } - } - - /** - * Specify the 3D coordinates to evaluate the gridded field. If not called, the grid points from - * the local plane will be used. - * - * @param x - * @param y - * @param z - */ - public void setEvaluationXYZ(double[] x, double[] y, double[] z) { - evaluateXYZ = new ArrayList<>(); - for (int i = 0; i < x.length; i++) { - evaluateXYZ.add(new Vector3(x[i], y[i], z[i])); - } - evaluateAtCustomPoints = true; - } - - public double[][] getRotation() { - return rotation.toArray(); - } - - public double[] getPlaneNormal() { - return uz.toArray(); - } - - /** - * Return a 4x4 transformation matrix. The top left 3x3 matrix is the rotation matrix. The top - * three entries in the right hand column are the translation vector. The bottom row is always 0 0 - * 0 1. - *

- * local coordinate system to global: - * - *

-   * {@code
-   * transformedPoint = rotation.mtxv(point).sub(translation);
-   * }
-   * 
- *

- * global coordinate system to local: - * - *

-   * {@code
-   * transformedPoint = rotation.mxv(point.add(translation));
-   * }
-   * 
- * - * @return - */ - public double[][] getTransformation() { - double[][] retArray = MatrixUtils.createRealIdentityMatrix(4).getData(); - - try { - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - retArray[i][j] = rotation.getElt(i, j); - } - retArray[i][3] = -translation.getElt(i); - } - } catch (SpiceException e) { - e.printStackTrace(); - } - return (retArray); - } - - /** - *

- * local coordinate system to global: - * - *

-   * {@code
-   * transformedPoint = rotation.mtxv(point).sub(translation);
-   * }
-   * 
- *

- * global coordinate system to local: - * - *

-   * {@code
-   * transformedPoint = rotation.mxv(point.add(translation));
-   * }
-   * 
- * - * @return - */ - public Pair getTransformationAsPair() { - return Pair.create(new Vector3(translation).negate(), new Matrix33(rotation)); - } - - /** - * Set the rotation matrix to transform between global and local coordinates. If not called, a - * rotation matrix will be found from a transformation that moves the input XYZ coordinates - * closest to a best fit plane. Call this AFTER {@link #setXYZ(double[], double[], double[])}. - * - */ - public void setRotation(double[][] rotation) { - try { - this.rotation = new Matrix33(rotation); - calculateTransformation(); - } catch (SpiceException e) { - e.printStackTrace(); - } - } - - public double[] getTranslation() { - return translation.toArray(); - } - - /** - * Set the translation vector to transform from local to global coordinates. If not called, - * translation vector will be set to the centroid of the input XYZ coordinates. Call this AFTER - * {@link #setXYZ(double[], double[], double[])}. - */ - public void setTranslation(double[] translation) { - if (translation != null) { - try { - this.translation = new Vector3(translation); - calculateTransformation(); - } catch (SpiceException e) { - e.printStackTrace(); - } - } - } - - /** - * Specify input field values at the coordinates supplied to - * {@link #setXYZ(double[], double[], double[])} - * - * @param fArray - */ - public void setField(double[] fArray) { - field = new ArrayList<>(); - for (double f : fArray) - field.add(f); - } - - /** - * If called, set the field to the height above plane. XYZ points are rotated to the local - * coordinate system and height above the plane (Z coordinate) is stored as the field to - * interpolate. - */ - public void setFieldToHeight() { - List transformed = globalToLocal(pointsList); - double[] heights = new double[transformed.size()]; - for (int i = 0; i < heights.length; i++) { - try { - heights[i] = transformed.get(i).getElt(2); - } catch (SpiceException e) { - e.printStackTrace(); - } - } - setField(heights); - } - - /** - * Field positions from the pointsList are transformed to a local plane coordinate system and - * regridded with GMTSurface. These points are then transformed back to the global coordinate - * system. - *

- * GMTSurface is run twice. Once with the field value substituted for z, and one with the - * transformed x, y, z to allow transforming back from the local plane to global coordinates. - * - * @return a double array of dimensions[7][nX][nY]. First six indices are Lat, Lon, Radius, X, Y, - * and Z. Last index is the field value at that position. - * @throws SpiceException - * @throws IOException - * @throws InterruptedException - * @throws FitsException - */ - public double[][][] regridField() - throws SpiceException, IOException, InterruptedException, FitsException { - - File tmpDir = new File(String.format("GMT-%d", System.currentTimeMillis())); - if (!tmpDir.exists()) - tmpDir.mkdirs(); - tmpDir.deleteOnExit(); - - List transformed = globalToLocal(pointsList); - double xmin = Double.MAX_VALUE; - double xmax = -xmin; - double ymin = Double.MAX_VALUE; - double ymax = -xmin; - for (Vector3 point : transformed) { - try { - double x = point.getElt(0); - double y = point.getElt(1); - if (x > xmax) - xmax = x; - if (x < xmin) - xmin = x; - if (y > ymax) - ymax = y; - if (y < ymin) - ymin = y; - } catch (SpiceException e) { - e.printStackTrace(); - } - } - - System.out.printf("Data extents %f/%f/%f/%f\n", xmin, xmax, ymin, ymax); - - ArrayList inputField = new ArrayList<>(); - for (int i = 0; i < transformed.size(); i++) { - Vector3 point = transformed.get(i); - Double value = field.get(i); - inputField.add(new Vector3(point.getElt(0), point.getElt(1), value)); - } - - xmax = (evaluateAtCustomPoints ? (halfSize + 1) : halfSize) * groundSampleDistance; - xmin = -xmax; - ymin = xmin; - ymax = xmax; - - // create a random 8 character string - String name = RandomStringUtils.randomAlphabetic(8); - String inputGMT = new File(tmpDir, name + "_gmt-input.bin").getPath(); - String outputNetCDF = new File(tmpDir, name + "_surface-output.grd").getPath(); - String outputFITS = new File(tmpDir, name + "_surface-output.fits").getPath(); - writeBinaryGMTInput(inputField, inputGMT); - - String command = String.format("GMTSurface %s %12.8f %f/%f/%f/%f %s %s %s", inputGMT, - groundSampleDistance, xmin, xmax, ymin, ymax, outputNetCDF, outputFITS, additionalGMTArgs); - - ProcessUtils.runProgramAndWait(command, null, true); - - DescriptiveStatistics xStats = new DescriptiveStatistics(); - DescriptiveStatistics yStats = new DescriptiveStatistics(); - - List surfaceField = new ArrayList<>(); - PiecewiseBicubicSplineInterpolatingFunction interpolator = - readGMTFits(outputFITS, surfaceField, xStats, yStats); - - if (globalXYZ == null) { - if (evaluateAtCustomPoints) { - globalXYZ = evaluateXYZ; - } else { - name = "height"; - inputGMT = new File(tmpDir, name + "_gmt-input.bin").getPath(); - outputNetCDF = new File(tmpDir, name + "_surface-output.grd").getPath(); - outputFITS = new File(tmpDir, name + "_surface-output.fits").getPath(); - writeBinaryGMTInput(transformed, inputGMT); - - command = String.format("GMTSurface %s %12.8f %f/%f/%f/%f %s %s %s", inputGMT, - groundSampleDistance, xmin, xmax, ymin, ymax, outputNetCDF, outputFITS, - additionalGMTArgs); - ProcessUtils.runProgramAndWait(command, null, false); - - List surfaceXYZ = new ArrayList<>(); - readGMTFits(outputFITS, surfaceXYZ, xStats, yStats); - - globalXYZ = localToGlobal(surfaceXYZ); - } - } - - if (evaluateAtCustomPoints) { - surfaceField = new ArrayList<>(); - List transformedEvaluationPoints = globalToLocal(evaluateXYZ); - for (Vector3 transformedEvaluationPoint : transformedEvaluationPoints) { - double x = transformedEvaluationPoint.getElt(0); - if (x < xStats.getMin()) { - System.err.printf("Warning: x value %g outside range [%g, %g], setting to %g\n", x, - xStats.getMin(), xStats.getMax(), xStats.getMin()); - x = xStats.getMin(); - } - if (x > xStats.getMax()) { - System.err.printf("Warning: x value %g outside range [%g, %g], setting to %g\n", x, - xStats.getMin(), xStats.getMax(), xStats.getMax()); - x = xStats.getMax(); - } - - double y = transformedEvaluationPoint.getElt(1); - if (y < yStats.getMin()) { - System.err.printf("Warning: y value %g outside range [%g, %g], setting to %g\n", y, - yStats.getMin(), yStats.getMax(), yStats.getMin()); - y = yStats.getMin(); - } - if (y > yStats.getMax()) { - System.err.printf("Warning: y value %g outside range [%g, %g], setting to %g\n", y, - yStats.getMin(), yStats.getMax(), yStats.getMax()); - y = yStats.getMax(); - } - - double z = interpolator.value(x, y); - surfaceField.add(new Vector3(x, y, z)); - } - } - - /*- - 0 - latitude (degrees) - 1 - longitude (degrees) - 2 - radius - 3 - vertex x - 4 - vertex y - 5 - vertex z - 6 - interpolated field value at vertex + /** + * Specify the dimensions and grid spacing of the 2D grid used by GMT. nX and nY must be odd and + * equal. + * + * @param nX + * @param nY + * @param groundSampleDistance */ - double[][][] returnArray = new double[7][nX][nY]; - - for (int i = 0; i < globalXYZ.size(); i++) { - LatitudinalCoordinates lc = new LatitudinalCoordinates(globalXYZ.get(i)); - int m = i / nX; - int n = i % nY; - - returnArray[0][m][n] = Math.toDegrees(lc.getLatitude()); - returnArray[1][m][n] = Math.toDegrees(lc.getLongitude()); - if (returnArray[1][m][n] < 0) - returnArray[1][m][n] += 360; - returnArray[2][m][n] = lc.getRadius(); - returnArray[3][m][n] = globalXYZ.get(i).getElt(0); - returnArray[4][m][n] = globalXYZ.get(i).getElt(1); - returnArray[5][m][n] = globalXYZ.get(i).getElt(2); - returnArray[6][m][n] = surfaceField.get(i).getElt(2); // Z coordinate is the regridded field + public GMTGridUtil(int nX, int nY, double groundSampleDistance) { + this.nX = nX; + this.nY = nY; + this.halfSize = (nX - 1) / 2; + checkDimensions(); + this.groundSampleDistance = groundSampleDistance; + this.evaluateAtCustomPoints = false; + this.additionalGMTArgs = ""; } - return returnArray; - } - - /** - * Finds the points in each bin and returns statistics using the supplied refValue rather than the - * mean. - * - * @param refValue - * @return - * @throws SpiceException - */ - public Map, DescriptiveStatistics> getStats(double[][] refValue) - throws SpiceException { - List transformed = globalToLocal(pointsList); - - ArrayList inputField = new ArrayList<>(); - for (int i = 0; i < transformed.size(); i++) { - Vector3 point = transformed.get(i); - Double value = field.get(i); - inputField.add(new Vector3(point.getElt(0), point.getElt(1), value)); + /** + * Specify the dimensions and grid spacing of the 2D grid used by GMT. + * + * @param halfSize grid dimensions are (2*halfSize +1)x(2*halfSize +1) + * @param groundSampleDistance + */ + public GMTGridUtil(int halfSize, double groundSampleDistance) { + this.halfSize = halfSize; + this.nX = halfSize * 2 + 1; + this.nY = halfSize * 2 + 1; + this.groundSampleDistance = groundSampleDistance; + this.evaluateAtCustomPoints = false; + this.additionalGMTArgs = ""; } - double xmin = -halfSize * groundSampleDistance; - double ymin = -halfSize * groundSampleDistance; - - Map, DescriptiveStatistics> binnedPoints = new HashMap<>(); - for (int i = 0; i < inputField.size(); i++) { - Vector3 point = inputField.get(i); - - double x = (point.getElt(0) - xmin) / groundSampleDistance; - double y = (point.getElt(1) - ymin) / groundSampleDistance; - - int m = (int) (Math.signum(x) * Math.floor(Math.abs(x))); - if (m < 0 || m >= nX) - continue; - int n = (int) (Math.signum(y) * Math.floor(Math.abs(y))); - if (n < 0 || n >= nY) - continue; - - Pair pair = Pair.create(m, n); - DescriptiveStatistics stats = binnedPoints.get(pair); - if (stats == null) { - stats = new DescriptiveStatistics(); - binnedPoints.put(pair, stats); - } - double residual = point.getElt(2) - refValue[m][n]; - stats.addValue(residual); - } - return binnedPoints; - } - - public void calculateTransformation() throws SpiceException { - int numPts = pointsList.size(); - - double[][] points = new double[3][numPts]; - for (int i = 0; i < numPts; i++) { - Vector3 translated = pointsList.get(i).sub(translation); - for (int j = 0; j < 3; j++) - points[j][i] = translated.getElt(j); - } - - // Follow the same logic as Mapola.fitPlaneToMapola() - - RealMatrix pointMatrix = new Array2DRowRealMatrix(points, false); - - // Now do SVD on this matrix - SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); - RealMatrix u = svd.getU(); - - // uz points normal to the plane and equals the eigenvector - // corresponding to the smallest eigenvalue of the V matrix - uz = new Vector3(u.getColumn(2)).hat(); - - setRotationFromUz(uz); - } - - /** - * Set the rotation matrix given the "up" vector. Call this AFTER - * {@link #setXYZ(double[], double[], double[])}. - * - * @param uz - * @throws SpiceException - */ - public void setRotationFromUz(Vector3 uz) throws SpiceException { - Vector3 ux, uy; - // Make sure uz points away from the asteroid rather than towards it - // by looking at the dot product of uz and the centroid. If dot product - // is negative, reverse uz. - if (translation.dot(uz) <= 0.0) - uz = uz.negate(); - - uz = uz.hat(); - - // new code for ux, uy, uz. Based on Bob Gaskell code in COMMON/ORIENT.f - if (uz.getElt(2) > 0.9998D) { - // z closest to pointing north (i.e. at north pole) - uz = new Vector3(0, 0, 1); - uy = new Vector3(0, 1, 0); - } else if (uz.getElt(2) < -0.9998D) { - // z closest to pointing south (i.e. at south pole) - uz = new Vector3(0, 0, -1); - uy = new Vector3(0, 1, 0); - } else { - // initial y vector to be orthogonal to z - uy = new Vector3(-uz.getElt(1), uz.getElt(0), 0).hat(); - } - - ux = uy.cross(uz); - uy = uz.cross(ux); - - rotation = new Matrix33(ux, uy, uz); - } - - /** - * Transform points in the local coordinate system to global - * - *

-   * {@code
-   * transformedPoint = rotation.mtxv(point).add(translation);
-   * }
-   * 
- * - * @return - */ - public List localToGlobal(List points) { - ArrayList transformed = new ArrayList<>(); - for (Vector3 point : points) { - Vector3 transformedPoint = rotation.mtxv(point).add(translation); - transformed.add(transformedPoint); - } - - return transformed; - } - - /** - * Transform points in the global coordinate system to local - * - *
-   * {@code
-   * transformedPoint = rotation.mxv(point.sub(translation));
-   * }
-   * 
- * - * @return - */ - public List globalToLocal(List points) { - ArrayList transformed = new ArrayList<>(); - for (Vector3 point : points) { - Vector3 transformedPoint = rotation.mxv(point.sub(translation)); - transformed.add(transformedPoint); - } - - return transformed; - } - - private void writeBinaryGMTInput(Collection points, String filename) { - try (DataOutputStream os = - new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)))) { - for (Vector3 point : points) { - for (int i = 0; i < 3; i++) - BinaryUtils.writeDoubleAndSwap(os, point.getElt(i)); - // GMTSurface expects 4 column - BinaryUtils.writeDoubleAndSwap(os, 1.); - } - - } catch (IOException | SpiceException e) { - e.printStackTrace(); - } - } - - private static PiecewiseBicubicSplineInterpolatingFunction readGMTFits(String fitsFile, - List points, DescriptiveStatistics xStats, DescriptiveStatistics yStats) - throws FitsException, IOException { - int[] axes = new int[3]; - - // load data from fits file - double[][][] data = FitsUtil.loadFits(fitsFile, axes); - - // indices into x,y,z components of position vector of fits file data array - int xIndex = 3; - int yIndex = 4; - int zIndex = 5; - - int numCols = axes[1]; - int numRows = axes[2]; - - double[] x = new double[numCols]; - double[] y = new double[numRows]; - double[][] z = new double[numCols][numRows]; - - xStats.clear(); - yStats.clear(); - - for (int n = 0; n < numCols; ++n) { - for (int m = 0; m < numRows; ++m) { - if (m == 0) { - x[n] = data[xIndex][m][n]; - xStats.addValue(x[n]); + private void checkDimensions() { + if (nX != nY) { + throw new IllegalArgumentException( + String.format("GMTGridUtil: nX (%d) and nY (%d) must be equal!\n", nX, nY)); } - if (n == 0) { - y[m] = data[yIndex][m][n]; - yStats.addValue(y[m]); + if (nX % 2 != 1) { + throw new IllegalArgumentException(String.format("GMTGridUtil: nX (%d) must be odd!\n", nX)); } - z[n][m] = data[zIndex][m][n]; - Vector3 thisPoint = new Vector3(x[n], y[m], z[n][m]); - points.add(thisPoint); - - // System.out.printf("%d %d %s\n", m, n, thisPoint); - } } - PiecewiseBicubicSplineInterpolator interpolator = new PiecewiseBicubicSplineInterpolator(); - return interpolator.interpolate(x, y, z); - } + public void setGMTArgs(String args) { + additionalGMTArgs = args; + } + /** + * Specify the 3D coordinates for the input field + * + * @param x + * @param y + * @param z + */ + public void setXYZ(double[] x, double[] y, double[] z) { + pointsList = new ArrayList<>(); + for (int i = 0; i < x.length; i++) { + pointsList.add(new Vector3(x[i], y[i], z[i])); + } + translation = new Vector3(); + for (Vector3 point : pointsList) translation = translation.add(point); + translation = translation.scale(1. / pointsList.size()); + + try { + calculateTransformation(); + } catch (SpiceException e) { + e.printStackTrace(); + System.exit(0); + } + } + + /** + * Specify the 3D coordinates to evaluate the gridded field. If not called, the grid points from + * the local plane will be used. + * + * @param x + * @param y + * @param z + */ + public void setEvaluationXYZ(double[] x, double[] y, double[] z) { + evaluateXYZ = new ArrayList<>(); + for (int i = 0; i < x.length; i++) { + evaluateXYZ.add(new Vector3(x[i], y[i], z[i])); + } + evaluateAtCustomPoints = true; + } + + public double[][] getRotation() { + return rotation.toArray(); + } + + public double[] getPlaneNormal() { + return uz.toArray(); + } + + /** + * Return a 4x4 transformation matrix. The top left 3x3 matrix is the rotation matrix. The top + * three entries in the right hand column are the translation vector. The bottom row is always 0 0 + * 0 1. + *

+ * local coordinate system to global: + * + *

+     * {@code
+     * transformedPoint = rotation.mtxv(point).sub(translation);
+     * }
+     * 
+ *

+ * global coordinate system to local: + * + *

+     * {@code
+     * transformedPoint = rotation.mxv(point.add(translation));
+     * }
+     * 
+ * + * @return + */ + public double[][] getTransformation() { + double[][] retArray = MatrixUtils.createRealIdentityMatrix(4).getData(); + + try { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + retArray[i][j] = rotation.getElt(i, j); + } + retArray[i][3] = -translation.getElt(i); + } + } catch (SpiceException e) { + e.printStackTrace(); + } + return (retArray); + } + + /** + *

+ * local coordinate system to global: + * + *

+     * {@code
+     * transformedPoint = rotation.mtxv(point).sub(translation);
+     * }
+     * 
+ *

+ * global coordinate system to local: + * + *

+     * {@code
+     * transformedPoint = rotation.mxv(point.add(translation));
+     * }
+     * 
+ * + * @return + */ + public Pair getTransformationAsPair() { + return Pair.create(new Vector3(translation).negate(), new Matrix33(rotation)); + } + + /** + * Set the rotation matrix to transform between global and local coordinates. If not called, a + * rotation matrix will be found from a transformation that moves the input XYZ coordinates + * closest to a best fit plane. Call this AFTER {@link #setXYZ(double[], double[], double[])}. + * + */ + public void setRotation(double[][] rotation) { + try { + this.rotation = new Matrix33(rotation); + calculateTransformation(); + } catch (SpiceException e) { + e.printStackTrace(); + } + } + + public double[] getTranslation() { + return translation.toArray(); + } + + /** + * Set the translation vector to transform from local to global coordinates. If not called, + * translation vector will be set to the centroid of the input XYZ coordinates. Call this AFTER + * {@link #setXYZ(double[], double[], double[])}. + */ + public void setTranslation(double[] translation) { + if (translation != null) { + try { + this.translation = new Vector3(translation); + calculateTransformation(); + } catch (SpiceException e) { + e.printStackTrace(); + } + } + } + + /** + * Specify input field values at the coordinates supplied to + * {@link #setXYZ(double[], double[], double[])} + * + * @param fArray + */ + public void setField(double[] fArray) { + field = new ArrayList<>(); + for (double f : fArray) field.add(f); + } + + /** + * If called, set the field to the height above plane. XYZ points are rotated to the local + * coordinate system and height above the plane (Z coordinate) is stored as the field to + * interpolate. + */ + public void setFieldToHeight() { + List transformed = globalToLocal(pointsList); + double[] heights = new double[transformed.size()]; + for (int i = 0; i < heights.length; i++) { + try { + heights[i] = transformed.get(i).getElt(2); + } catch (SpiceException e) { + e.printStackTrace(); + } + } + setField(heights); + } + + /** + * Field positions from the pointsList are transformed to a local plane coordinate system and + * regridded with GMTSurface. These points are then transformed back to the global coordinate + * system. + *

+ * GMTSurface is run twice. Once with the field value substituted for z, and one with the + * transformed x, y, z to allow transforming back from the local plane to global coordinates. + * + * @return a double array of dimensions[7][nX][nY]. First six indices are Lat, Lon, Radius, X, Y, + * and Z. Last index is the field value at that position. + * @throws SpiceException + * @throws IOException + * @throws InterruptedException + * @throws FitsException + */ + public double[][][] regridField() throws SpiceException, IOException, InterruptedException, FitsException { + + File tmpDir = new File(String.format("GMT-%d", System.currentTimeMillis())); + if (!tmpDir.exists()) tmpDir.mkdirs(); + tmpDir.deleteOnExit(); + + List transformed = globalToLocal(pointsList); + double xmin = Double.MAX_VALUE; + double xmax = -xmin; + double ymin = Double.MAX_VALUE; + double ymax = -xmin; + for (Vector3 point : transformed) { + try { + double x = point.getElt(0); + double y = point.getElt(1); + if (x > xmax) xmax = x; + if (x < xmin) xmin = x; + if (y > ymax) ymax = y; + if (y < ymin) ymin = y; + } catch (SpiceException e) { + e.printStackTrace(); + } + } + + System.out.printf("Data extents %f/%f/%f/%f\n", xmin, xmax, ymin, ymax); + + ArrayList inputField = new ArrayList<>(); + for (int i = 0; i < transformed.size(); i++) { + Vector3 point = transformed.get(i); + Double value = field.get(i); + inputField.add(new Vector3(point.getElt(0), point.getElt(1), value)); + } + + xmax = (evaluateAtCustomPoints ? (halfSize + 1) : halfSize) * groundSampleDistance; + xmin = -xmax; + ymin = xmin; + ymax = xmax; + + // create a random 8 character string + String name = RandomStringUtils.randomAlphabetic(8); + String inputGMT = new File(tmpDir, name + "_gmt-input.bin").getPath(); + String outputNetCDF = new File(tmpDir, name + "_surface-output.grd").getPath(); + String outputFITS = new File(tmpDir, name + "_surface-output.fits").getPath(); + writeBinaryGMTInput(inputField, inputGMT); + + String command = String.format( + "GMTSurface %s %12.8f %f/%f/%f/%f %s %s %s", + inputGMT, groundSampleDistance, xmin, xmax, ymin, ymax, outputNetCDF, outputFITS, additionalGMTArgs); + + ProcessUtils.runProgramAndWait(command, null, true); + + DescriptiveStatistics xStats = new DescriptiveStatistics(); + DescriptiveStatistics yStats = new DescriptiveStatistics(); + + List surfaceField = new ArrayList<>(); + PiecewiseBicubicSplineInterpolatingFunction interpolator = + readGMTFits(outputFITS, surfaceField, xStats, yStats); + + if (globalXYZ == null) { + if (evaluateAtCustomPoints) { + globalXYZ = evaluateXYZ; + } else { + name = "height"; + inputGMT = new File(tmpDir, name + "_gmt-input.bin").getPath(); + outputNetCDF = new File(tmpDir, name + "_surface-output.grd").getPath(); + outputFITS = new File(tmpDir, name + "_surface-output.fits").getPath(); + writeBinaryGMTInput(transformed, inputGMT); + + command = String.format( + "GMTSurface %s %12.8f %f/%f/%f/%f %s %s %s", + inputGMT, + groundSampleDistance, + xmin, + xmax, + ymin, + ymax, + outputNetCDF, + outputFITS, + additionalGMTArgs); + ProcessUtils.runProgramAndWait(command, null, false); + + List surfaceXYZ = new ArrayList<>(); + readGMTFits(outputFITS, surfaceXYZ, xStats, yStats); + + globalXYZ = localToGlobal(surfaceXYZ); + } + } + + if (evaluateAtCustomPoints) { + surfaceField = new ArrayList<>(); + List transformedEvaluationPoints = globalToLocal(evaluateXYZ); + for (Vector3 transformedEvaluationPoint : transformedEvaluationPoints) { + double x = transformedEvaluationPoint.getElt(0); + if (x < xStats.getMin()) { + System.err.printf( + "Warning: x value %g outside range [%g, %g], setting to %g\n", + x, xStats.getMin(), xStats.getMax(), xStats.getMin()); + x = xStats.getMin(); + } + if (x > xStats.getMax()) { + System.err.printf( + "Warning: x value %g outside range [%g, %g], setting to %g\n", + x, xStats.getMin(), xStats.getMax(), xStats.getMax()); + x = xStats.getMax(); + } + + double y = transformedEvaluationPoint.getElt(1); + if (y < yStats.getMin()) { + System.err.printf( + "Warning: y value %g outside range [%g, %g], setting to %g\n", + y, yStats.getMin(), yStats.getMax(), yStats.getMin()); + y = yStats.getMin(); + } + if (y > yStats.getMax()) { + System.err.printf( + "Warning: y value %g outside range [%g, %g], setting to %g\n", + y, yStats.getMin(), yStats.getMax(), yStats.getMax()); + y = yStats.getMax(); + } + + double z = interpolator.value(x, y); + surfaceField.add(new Vector3(x, y, z)); + } + } + + /*- + 0 - latitude (degrees) + 1 - longitude (degrees) + 2 - radius + 3 - vertex x + 4 - vertex y + 5 - vertex z + 6 - interpolated field value at vertex + */ + double[][][] returnArray = new double[7][nX][nY]; + + for (int i = 0; i < globalXYZ.size(); i++) { + LatitudinalCoordinates lc = new LatitudinalCoordinates(globalXYZ.get(i)); + int m = i / nX; + int n = i % nY; + + returnArray[0][m][n] = Math.toDegrees(lc.getLatitude()); + returnArray[1][m][n] = Math.toDegrees(lc.getLongitude()); + if (returnArray[1][m][n] < 0) returnArray[1][m][n] += 360; + returnArray[2][m][n] = lc.getRadius(); + returnArray[3][m][n] = globalXYZ.get(i).getElt(0); + returnArray[4][m][n] = globalXYZ.get(i).getElt(1); + returnArray[5][m][n] = globalXYZ.get(i).getElt(2); + returnArray[6][m][n] = surfaceField.get(i).getElt(2); // Z coordinate is the regridded field + } + + return returnArray; + } + + /** + * Finds the points in each bin and returns statistics using the supplied refValue rather than the + * mean. + * + * @param refValue + * @return + * @throws SpiceException + */ + public Map, DescriptiveStatistics> getStats(double[][] refValue) throws SpiceException { + List transformed = globalToLocal(pointsList); + + ArrayList inputField = new ArrayList<>(); + for (int i = 0; i < transformed.size(); i++) { + Vector3 point = transformed.get(i); + Double value = field.get(i); + inputField.add(new Vector3(point.getElt(0), point.getElt(1), value)); + } + + double xmin = -halfSize * groundSampleDistance; + double ymin = -halfSize * groundSampleDistance; + + Map, DescriptiveStatistics> binnedPoints = new HashMap<>(); + for (int i = 0; i < inputField.size(); i++) { + Vector3 point = inputField.get(i); + + double x = (point.getElt(0) - xmin) / groundSampleDistance; + double y = (point.getElt(1) - ymin) / groundSampleDistance; + + int m = (int) (Math.signum(x) * Math.floor(Math.abs(x))); + if (m < 0 || m >= nX) continue; + int n = (int) (Math.signum(y) * Math.floor(Math.abs(y))); + if (n < 0 || n >= nY) continue; + + Pair pair = Pair.create(m, n); + DescriptiveStatistics stats = binnedPoints.get(pair); + if (stats == null) { + stats = new DescriptiveStatistics(); + binnedPoints.put(pair, stats); + } + double residual = point.getElt(2) - refValue[m][n]; + stats.addValue(residual); + } + return binnedPoints; + } + + public void calculateTransformation() throws SpiceException { + int numPts = pointsList.size(); + + double[][] points = new double[3][numPts]; + for (int i = 0; i < numPts; i++) { + Vector3 translated = pointsList.get(i).sub(translation); + for (int j = 0; j < 3; j++) points[j][i] = translated.getElt(j); + } + + // Follow the same logic as Mapola.fitPlaneToMapola() + + RealMatrix pointMatrix = new Array2DRowRealMatrix(points, false); + + // Now do SVD on this matrix + SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); + RealMatrix u = svd.getU(); + + // uz points normal to the plane and equals the eigenvector + // corresponding to the smallest eigenvalue of the V matrix + uz = new Vector3(u.getColumn(2)).hat(); + + setRotationFromUz(uz); + } + + /** + * Set the rotation matrix given the "up" vector. Call this AFTER + * {@link #setXYZ(double[], double[], double[])}. + * + * @param uz + * @throws SpiceException + */ + public void setRotationFromUz(Vector3 uz) throws SpiceException { + Vector3 ux, uy; + // Make sure uz points away from the asteroid rather than towards it + // by looking at the dot product of uz and the centroid. If dot product + // is negative, reverse uz. + if (translation.dot(uz) <= 0.0) uz = uz.negate(); + + uz = uz.hat(); + + // new code for ux, uy, uz. Based on Bob Gaskell code in COMMON/ORIENT.f + if (uz.getElt(2) > 0.9998D) { + // z closest to pointing north (i.e. at north pole) + uz = new Vector3(0, 0, 1); + uy = new Vector3(0, 1, 0); + } else if (uz.getElt(2) < -0.9998D) { + // z closest to pointing south (i.e. at south pole) + uz = new Vector3(0, 0, -1); + uy = new Vector3(0, 1, 0); + } else { + // initial y vector to be orthogonal to z + uy = new Vector3(-uz.getElt(1), uz.getElt(0), 0).hat(); + } + + ux = uy.cross(uz); + uy = uz.cross(ux); + + rotation = new Matrix33(ux, uy, uz); + } + + /** + * Transform points in the local coordinate system to global + * + *

+     * {@code
+     * transformedPoint = rotation.mtxv(point).add(translation);
+     * }
+     * 
+ * + * @return + */ + public List localToGlobal(List points) { + ArrayList transformed = new ArrayList<>(); + for (Vector3 point : points) { + Vector3 transformedPoint = rotation.mtxv(point).add(translation); + transformed.add(transformedPoint); + } + + return transformed; + } + + /** + * Transform points in the global coordinate system to local + * + *
+     * {@code
+     * transformedPoint = rotation.mxv(point.sub(translation));
+     * }
+     * 
+ * + * @return + */ + public List globalToLocal(List points) { + ArrayList transformed = new ArrayList<>(); + for (Vector3 point : points) { + Vector3 transformedPoint = rotation.mxv(point.sub(translation)); + transformed.add(transformedPoint); + } + + return transformed; + } + + private void writeBinaryGMTInput(Collection points, String filename) { + try (DataOutputStream os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)))) { + for (Vector3 point : points) { + for (int i = 0; i < 3; i++) BinaryUtils.writeDoubleAndSwap(os, point.getElt(i)); + // GMTSurface expects 4 column + BinaryUtils.writeDoubleAndSwap(os, 1.); + } + + } catch (IOException | SpiceException e) { + e.printStackTrace(); + } + } + + private static PiecewiseBicubicSplineInterpolatingFunction readGMTFits( + String fitsFile, List points, DescriptiveStatistics xStats, DescriptiveStatistics yStats) + throws FitsException, IOException { + int[] axes = new int[3]; + + // load data from fits file + double[][][] data = FitsUtil.loadFits(fitsFile, axes); + + // indices into x,y,z components of position vector of fits file data array + int xIndex = 3; + int yIndex = 4; + int zIndex = 5; + + int numCols = axes[1]; + int numRows = axes[2]; + + double[] x = new double[numCols]; + double[] y = new double[numRows]; + double[][] z = new double[numCols][numRows]; + + xStats.clear(); + yStats.clear(); + + for (int n = 0; n < numCols; ++n) { + for (int m = 0; m < numRows; ++m) { + if (m == 0) { + x[n] = data[xIndex][m][n]; + xStats.addValue(x[n]); + } + if (n == 0) { + y[m] = data[yIndex][m][n]; + yStats.addValue(y[m]); + } + z[n][m] = data[zIndex][m][n]; + Vector3 thisPoint = new Vector3(x[n], y[m], z[n][m]); + points.add(thisPoint); + + // System.out.printf("%d %d %s\n", m, n, thisPoint); + } + } + + PiecewiseBicubicSplineInterpolator interpolator = new PiecewiseBicubicSplineInterpolator(); + return interpolator.interpolate(x, y, z); + } } diff --git a/src/main/java/terrasaur/utils/ICQUtils.java b/src/main/java/terrasaur/utils/ICQUtils.java index 3deb20a..3d6066b 100644 --- a/src/main/java/terrasaur/utils/ICQUtils.java +++ b/src/main/java/terrasaur/utils/ICQUtils.java @@ -22,13 +22,12 @@ */ 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; +import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class ICQUtils { private static final Logger logger = LogManager.getLogger(); @@ -87,7 +86,7 @@ public class ICQUtils { return u; } - public static void writeICQ(int q, double[][][][] vec, String filename){ + public static void writeICQ(int q, double[][][][] vec, String filename) { try (PrintWriter out = new PrintWriter(new FileWriter(filename))) { out.println(q); @@ -104,8 +103,5 @@ public class ICQUtils { } catch (IOException e) { logger.error(e); } - } - - } diff --git a/src/main/java/terrasaur/utils/JCommanderUsage.java b/src/main/java/terrasaur/utils/JCommanderUsage.java index 0c24653..6848c16 100644 --- a/src/main/java/terrasaur/utils/JCommanderUsage.java +++ b/src/main/java/terrasaur/utils/JCommanderUsage.java @@ -22,10 +22,6 @@ */ package terrasaur.utils; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterDescription; @@ -34,337 +30,344 @@ import com.beust.jcommander.Parameters; import com.beust.jcommander.Strings; import com.beust.jcommander.WrappedParameter; import com.beust.jcommander.internal.Lists; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; public class JCommanderUsage { - private final JCommander jc; - private final String programName; - private int m_columnSize = 100; + private final JCommander jc; + private final String programName; + private int m_columnSize = 100; - private Comparator m_parameterDescriptionComparator = - new Comparator() { - @Override - public int compare(ParameterDescription p0, ParameterDescription p1) { - return p0.getLongestName().compareTo(p1.getLongestName()); - } - }; + private Comparator m_parameterDescriptionComparator = + new Comparator() { + @Override + public int compare(ParameterDescription p0, ParameterDescription p1) { + return p0.getLongestName().compareTo(p1.getLongestName()); + } + }; - private Comparator m_parameterOrderComparator = - new Comparator() { - @Override - public int compare(ParameterDescription p0, ParameterDescription p1) { + private Comparator m_parameterOrderComparator = + new Comparator() { + @Override + public int compare(ParameterDescription p0, ParameterDescription p1) { - // compare by order - Parameter orderp0 = p0.getParameterAnnotation(); - Parameter orderp1 = p1.getParameterAnnotation(); - return orderp0.order() - orderp1.order(); - // return p0.getLongestName().compareTo(p1.getLongestName()); - } - }; + // compare by order + Parameter orderp0 = p0.getParameterAnnotation(); + Parameter orderp1 = p1.getParameterAnnotation(); + return orderp0.order() - orderp1.order(); + // return p0.getLongestName().compareTo(p1.getLongestName()); + } + }; - public JCommanderUsage(JCommander jc) { - this(jc, ""); - } - - public JCommanderUsage(JCommander jc, String programName) { - this.jc = jc; - this.programName = programName; - } - - /** - * Display the usage for this command. - */ - public void usage(String commandName) { - StringBuilder sb = new StringBuilder(); - usage(commandName, sb); - JCommander.getConsole().println(sb.toString()); - } - - /** - * Store the help for the command in the passed string builder. - */ - public void usage(String commandName, StringBuilder out) { - usage(commandName, out, ""); - } - - /** - * Store the help for the command in the passed string builder, indenting every line with - * "indent". - */ - public void usage(String commandName, StringBuilder out, String indent) { - String description = null; - try { - description = jc.getCommandDescription(commandName); - } catch (ParameterException e) { - // Simplest way to handle problem with fetching descriptions for the main command. - // In real implementations we would have done this another way. + public JCommanderUsage(JCommander jc) { + this(jc, ""); } - if (description != null) { - out.append(indent).append(description); - out.append("\n"); + public JCommanderUsage(JCommander jc, String programName) { + this.jc = jc; + this.programName = programName; } - // PROBLEM : JCommander jcc = jc.findCommandByAlias(commandName); // Wops, not public! - JCommander jcc = findCommandByAlias(commandName); - if (jcc != null) { - jcc.usage(out, indent); - } - } - private JCommander findCommandByAlias(String commandOrAlias) { - // Wops, not public and return ProgramName class that neither is public. - // No way to get around this. - // PROBLEM : JCommander.ProgramName progName = jc.findProgramName(commandOrAlias); - - // So, then it turns out we cannot mimic the functionality implemented in usage for - // printing command usage :( - /* - * if(progName == null) { return null; } else { JCommander jc = this.findCommand(progName); - * if(jc == null) { throw new IllegalStateException( - * "There appears to be inconsistency in the internal command database. This is likely a bug. Please report." - * ); } else { return jc; } } + /** + * Display the usage for this command. */ - // Lets go for the solution which is available to us and ignore the logic implemented in - // JCommander for this lookup. - return jc.getCommands().get(commandOrAlias); - } + public void usage(String commandName) { + StringBuilder sb = new StringBuilder(); + usage(commandName, sb); + JCommander.getConsole().println(sb.toString()); + } - /** - * Allow user to pre-pend a command description before showing the usage and all the options. - * Indent is an integer count of the number of spaces of indentation desired for the Parameter - * arguments. - * - * @param out - * @param indent - * @param commandDesc - */ - public void usage(StringBuilder out, int indent, String commandDesc) { - - out.append(commandDesc); - usage(out, s(indent)); - - } - - public void usage(StringBuilder out, String indent) { - - // Why is this done on this stage of the process? - // Looks like something that should have been done earlier on? - // Anyway the createDescriptions() method is private and not possible to - // trigger from the outside. - // I haven't spend time on considering the consequences of not executing the method. - /* PROBLEM : if (m_descriptions == null) createDescriptions(); */ - - boolean hasCommands = !jc.getCommands().isEmpty(); - - // - // First line of the usage - // - - // The JCommander does not provide a getProgramName() method and therefore - // makes it impossible for other usage implementations to use it. - // My first idea was to use the reflection api to change the access level of - // the m_programName attribute, but then I saw that the ProgramName class is - // private as well :( - // Of course it is possible to set an alternative program name in the usage - // implementation, but that is kind of a second best solution. - - /* - * PROBLEM : String programName = m_programName != null ? m_programName.getDisplayName() : - * "
"; + /** + * Store the help for the command in the passed string builder. */ - // String programName = ""; - // - // out.append(indent).append("Usage: ").append(programName).append(" [options]"); - // if (hasCommands) { - // out.append(indent).append(" [command] [command options]"); - // } - if (jc.getMainParameterDescription() != null) { - out.append(" ").append(jc.getMainParameterDescription()); + public void usage(String commandName, StringBuilder out) { + usage(commandName, out, ""); } - out.append("\n"); - // - // Align the descriptions at the "longestName" column - // - int longestName = 0; - List sorted = Lists.newArrayList(); - for (ParameterDescription pd : jc.getParameters()) { - if (!pd.getParameter().hidden()) { - sorted.add(pd); - // + to have an extra space between the name and the description - int length = pd.getNames().length() + 2; - if (length > longestName) { - longestName = length; + /** + * Store the help for the command in the passed string builder, indenting every line with + * "indent". + */ + public void usage(String commandName, StringBuilder out, String indent) { + String description = null; + try { + description = jc.getCommandDescription(commandName); + } catch (ParameterException e) { + // Simplest way to handle problem with fetching descriptions for the main command. + // In real implementations we would have done this another way. + } + + if (description != null) { + out.append(indent).append(description); + out.append("\n"); + } + // PROBLEM : JCommander jcc = jc.findCommandByAlias(commandName); // Wops, not public! + JCommander jcc = findCommandByAlias(commandName); + if (jcc != null) { + jcc.usage(out, indent); } - } } - // - // Sort the options - // - // Collections.sort(sorted, getParameterDescriptionComparator()); + private JCommander findCommandByAlias(String commandOrAlias) { + // Wops, not public and return ProgramName class that neither is public. + // No way to get around this. + // PROBLEM : JCommander.ProgramName progName = jc.findProgramName(commandOrAlias); - Collections.sort(sorted, getParameterOrderComparator()); - - // - // Display all the names and descriptions - // - int descriptionIndent = 0; - if (sorted.size() > 0) { - // out.append(indent).append(" Options:\n"); - out.append(" Options:\n"); - } - for (ParameterDescription pd : sorted) { - WrappedParameter parameter = pd.getParameter(); - // if (!pd.getNames().contains("--")) { - // namePad = " "; - // } - out.append(indent) - // .append(" ") - // .append(parameter.required() ? "* " : " ") - .append(pd.getNames()); - // .append(" ") - // .append(indent) - // .append(s(descriptionIndent)); - // int indentCount = indent.length() + descriptionIndent; - - int indentCount = longestName; - indentCount = pd.getNames().length(); - if (pd.getNames().length() < longestName) { - indentCount = longestName - pd.getNames().length() - 1; - } - // wrapDescription(out, indentCount, pd.getDescription()); - wrapDescriptionInLine(out, indentCount, longestName + 4, pd.getDescription()); - Object def = pd.getDefault(); - if (pd.isDynamicParameter()) { - out.append("\n").append(s(indentCount + 1)).append("Syntax: ").append(parameter.names()[0]) - .append("key").append(parameter.getAssignment()).append("value"); - } - if (def != null) { - String displayedDef = - Strings.isStringEmpty(def.toString()) ? "" : def.toString(); - out.append("\n") - // .append(s(indentCount + 1)) - .append(s(longestName + 4)).append("Default: ") - .append(parameter.password() ? "********" : displayedDef); - } - out.append("\n"); - out.append("\n"); + // So, then it turns out we cannot mimic the functionality implemented in usage for + // printing command usage :( + /* + * if(progName == null) { return null; } else { JCommander jc = this.findCommand(progName); + * if(jc == null) { throw new IllegalStateException( + * "There appears to be inconsistency in the internal command database. This is likely a bug. Please report." + * ); } else { return jc; } } + */ + // Lets go for the solution which is available to us and ignore the logic implemented in + // JCommander for this lookup. + return jc.getCommands().get(commandOrAlias); } - // - // If commands were specified, show them as well - // - if (hasCommands) { - out.append(" Commands:\n"); - // The magic value 3 is the number of spaces between the name of the option - // and its description - for (Map.Entry commands : jc.getCommands().entrySet()) { - Object arg = commands.getValue().getObjects().get(0); - Parameters p = arg.getClass().getAnnotation(Parameters.class); - // I'm not sure why, but this simply doesn't work in my test project. - // But this is not important in this POC - // if (!p.hidden()) { - String dispName = commands.getKey(); - out.append(indent).append(" " + dispName); // + s(spaceCount) + - // getCommandDescription(progName.name) + - // "\n"); + /** + * Allow user to pre-pend a command description before showing the usage and all the options. + * Indent is an integer count of the number of spaces of indentation desired for the Parameter + * arguments. + * + * @param out + * @param indent + * @param commandDesc + */ + public void usage(StringBuilder out, int indent, String commandDesc) { - // Options for this command - usage(dispName, out, " "); - out.append("\n"); + out.append(commandDesc); + usage(out, s(indent)); + } + + public void usage(StringBuilder out, String indent) { + + // Why is this done on this stage of the process? + // Looks like something that should have been done earlier on? + // Anyway the createDescriptions() method is private and not possible to + // trigger from the outside. + // I haven't spend time on considering the consequences of not executing the method. + /* PROBLEM : if (m_descriptions == null) createDescriptions(); */ + + boolean hasCommands = !jc.getCommands().isEmpty(); + + // + // First line of the usage + // + + // The JCommander does not provide a getProgramName() method and therefore + // makes it impossible for other usage implementations to use it. + // My first idea was to use the reflection api to change the access level of + // the m_programName attribute, but then I saw that the ProgramName class is + // private as well :( + // Of course it is possible to set an alternative program name in the usage + // implementation, but that is kind of a second best solution. + + /* + * PROBLEM : String programName = m_programName != null ? m_programName.getDisplayName() : + * "
"; + */ + // String programName = ""; + // + // out.append(indent).append("Usage: ").append(programName).append(" [options]"); + // if (hasCommands) { + // out.append(indent).append(" [command] [command options]"); // } - } - } - } - - private void wrapDescription(StringBuilder out, int indent, String description) { - int max = getColumnSize(); - String[] words = description.split(" "); - - // indent before adding any words. - // out.append(s(indent)); - int current = indent; - int i = 0; - while (i < words.length) { - String word = words[i]; - if (word.length() > max || current + word.length() <= max) { - out.append(" ").append(word); - current += word.length() + 1; - } else { - out.append("\n").append(s(indent + 1)).append(word); - current = indent; - } - i++; - } - } - - /** - * Wrap the description for the case where the user wants the description to start in-line with - * the command-line argument. - * - * @param out - * @param firstIndent - * @param indent - * @param description - */ - private void wrapDescriptionInLine(StringBuilder out, int firstIndent, int indent, - String description) { - int max = getColumnSize(); - description = description.replaceAll("\\n", " .CR."); - String[] words = description.split(" "); - - // first indent before adding any words. - out.append(s(firstIndent)); - int current = indent; - int i = 0; - while (i < words.length) { - String word = words[i]; - if (word.equals(".CR.")) { - out.append("\n").append(s(indent - 1)); - current = indent; - } else { - if (word.length() > max || current + word.length() <= max) { - out.append(" ").append(word); - current += word.length() + 1; - } else { - out.append("\n").append(s(indent)).append(word); - current = indent; + if (jc.getMainParameterDescription() != null) { + out.append(" ").append(jc.getMainParameterDescription()); } - } - i++; - } - } + out.append("\n"); - private Comparator getParameterDescriptionComparator() { - return m_parameterDescriptionComparator; - } + // + // Align the descriptions at the "longestName" column + // + int longestName = 0; + List sorted = Lists.newArrayList(); + for (ParameterDescription pd : jc.getParameters()) { + if (!pd.getParameter().hidden()) { + sorted.add(pd); + // + to have an extra space between the name and the description + int length = pd.getNames().length() + 2; + if (length > longestName) { + longestName = length; + } + } + } - private Comparator getParameterOrderComparator() { - return m_parameterOrderComparator; - } + // + // Sort the options + // + // Collections.sort(sorted, getParameterDescriptionComparator()); - public void setParameterDescriptionComparator(Comparator c) { - m_parameterDescriptionComparator = c; - } + Collections.sort(sorted, getParameterOrderComparator()); - public void setColumnSize(int m_columnSize) { - this.m_columnSize = m_columnSize; - } + // + // Display all the names and descriptions + // + int descriptionIndent = 0; + if (sorted.size() > 0) { + // out.append(indent).append(" Options:\n"); + out.append(" Options:\n"); + } + for (ParameterDescription pd : sorted) { + WrappedParameter parameter = pd.getParameter(); + // if (!pd.getNames().contains("--")) { + // namePad = " "; + // } + out.append(indent) + // .append(" ") + // .append(parameter.required() ? "* " : " ") + .append(pd.getNames()); + // .append(" ") + // .append(indent) + // .append(s(descriptionIndent)); + // int indentCount = indent.length() + descriptionIndent; - public int getColumnSize() { - return m_columnSize; - } + int indentCount = longestName; + indentCount = pd.getNames().length(); + if (pd.getNames().length() < longestName) { + indentCount = longestName - pd.getNames().length() - 1; + } + // wrapDescription(out, indentCount, pd.getDescription()); + wrapDescriptionInLine(out, indentCount, longestName + 4, pd.getDescription()); + Object def = pd.getDefault(); + if (pd.isDynamicParameter()) { + out.append("\n") + .append(s(indentCount + 1)) + .append("Syntax: ") + .append(parameter.names()[0]) + .append("key") + .append(parameter.getAssignment()) + .append("value"); + } + if (def != null) { + String displayedDef = Strings.isStringEmpty(def.toString()) ? "" : def.toString(); + out.append("\n") + // .append(s(indentCount + 1)) + .append(s(longestName + 4)) + .append("Default: ") + .append(parameter.password() ? "********" : displayedDef); + } + out.append("\n"); + out.append("\n"); + } - /** - * @return n spaces - */ - private String s(int count) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < count; i++) { - result.append(" "); + // + // If commands were specified, show them as well + // + if (hasCommands) { + out.append(" Commands:\n"); + // The magic value 3 is the number of spaces between the name of the option + // and its description + for (Map.Entry commands : jc.getCommands().entrySet()) { + Object arg = commands.getValue().getObjects().get(0); + Parameters p = arg.getClass().getAnnotation(Parameters.class); + // I'm not sure why, but this simply doesn't work in my test project. + // But this is not important in this POC + // if (!p.hidden()) { + String dispName = commands.getKey(); + out.append(indent).append(" " + dispName); // + s(spaceCount) + + // getCommandDescription(progName.name) + + // "\n"); + + // Options for this command + usage(dispName, out, " "); + out.append("\n"); + // } + } + } } - return result.toString(); - } + private void wrapDescription(StringBuilder out, int indent, String description) { + int max = getColumnSize(); + String[] words = description.split(" "); + + // indent before adding any words. + // out.append(s(indent)); + int current = indent; + int i = 0; + while (i < words.length) { + String word = words[i]; + if (word.length() > max || current + word.length() <= max) { + out.append(" ").append(word); + current += word.length() + 1; + } else { + out.append("\n").append(s(indent + 1)).append(word); + current = indent; + } + i++; + } + } + + /** + * Wrap the description for the case where the user wants the description to start in-line with + * the command-line argument. + * + * @param out + * @param firstIndent + * @param indent + * @param description + */ + private void wrapDescriptionInLine(StringBuilder out, int firstIndent, int indent, String description) { + int max = getColumnSize(); + description = description.replaceAll("\\n", " .CR."); + String[] words = description.split(" "); + + // first indent before adding any words. + out.append(s(firstIndent)); + int current = indent; + int i = 0; + while (i < words.length) { + String word = words[i]; + if (word.equals(".CR.")) { + out.append("\n").append(s(indent - 1)); + current = indent; + } else { + if (word.length() > max || current + word.length() <= max) { + out.append(" ").append(word); + current += word.length() + 1; + } else { + out.append("\n").append(s(indent)).append(word); + current = indent; + } + } + i++; + } + } + + private Comparator getParameterDescriptionComparator() { + return m_parameterDescriptionComparator; + } + + private Comparator getParameterOrderComparator() { + return m_parameterOrderComparator; + } + + public void setParameterDescriptionComparator(Comparator c) { + m_parameterDescriptionComparator = c; + } + + public void setColumnSize(int m_columnSize) { + this.m_columnSize = m_columnSize; + } + + public int getColumnSize() { + return m_columnSize; + } + + /** + * @return n spaces + */ + private String s(int count) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) { + result.append(" "); + } + + return result.toString(); + } } diff --git a/src/main/java/terrasaur/utils/Log4j2Configurator.java b/src/main/java/terrasaur/utils/Log4j2Configurator.java index 443753c..2082142 100644 --- a/src/main/java/terrasaur/utils/Log4j2Configurator.java +++ b/src/main/java/terrasaur/utils/Log4j2Configurator.java @@ -39,165 +39,180 @@ import org.apache.logging.log4j.core.layout.PatternLayout; /** * A simple configuration class. - * + * * Default settings: *
    *
  • Pattern is "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c{1}:%L] %msg%n%throwable"
    * (e.g. 2021-11-09 19:32:37.119 INFO [LoggingTest:25] Level INFO)
  • *
  • Log level is {@link Level#INFO}
  • *
- * + * * @author nairah1 * */ public class Log4j2Configurator { - private PatternLayout layout; - private Map fileAppenders; + private PatternLayout layout; + private Map fileAppenders; - private static Log4j2Configurator instance = null; + private static Log4j2Configurator instance = null; - /** - * Get an instance of this singleton class. - * - * @return - */ - synchronized public static Log4j2Configurator getInstance() { - if (instance == null) { - instance = new Log4j2Configurator(); - } - return instance; - } - - private Log4j2Configurator() { - final LoggerContext loggerContext = LoggerContext.getContext(false); - final Configuration config = loggerContext.getConfiguration(); - layout = PatternLayout.newBuilder().withPattern(DefaultConfiguration.DEFAULT_PATTERN) - .withConfiguration(config).build(); - fileAppenders = new HashMap<>(); - setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c{1}:%L] %msg%n%throwable"); - setLevel(Level.INFO); - } - - /** - * - * @return a map of logger names to {@link LoggerConfig} - */ - private Map getLoggerMap() { - final LoggerContext loggerContext = LoggerContext.getContext(false); - final Configuration config = loggerContext.getConfiguration(); - - Map loggerMap = new HashMap<>(config.getLoggers()); - loggerMap.put(LogManager.getRootLogger().getName(), - config.getLoggerConfig(LogManager.getRootLogger().getName())); - return Collections.unmodifiableMap(loggerMap); - } - - /** - * Append log to named file, or create it if it doesn't exist. - * - * @param filename - */ - public void addFile(String filename) { - final LoggerContext loggerContext = LoggerContext.getContext(false); - Map loggerMap = getLoggerMap(); - - FileAppender appender = FileAppender.newBuilder().setName(filename).withFileName(filename) - .setLayout(layout).build(); - appender.start(); - - for (String loggerName : loggerMap.keySet()) { - LoggerConfig loggerConfig = loggerMap.get(loggerName); - loggerConfig.addAppender(appender, null, null); - } - loggerContext.updateLoggers(); - fileAppenders.put(filename, appender); - } - - /** - * Stop logging to named file. - * - * @param filename - */ - public void removeFile(String filename) { - if (fileAppenders.containsKey(filename)) { - final LoggerContext loggerContext = LoggerContext.getContext(false); - Map loggerMap = getLoggerMap(); - - FileAppender appender = fileAppenders.get(filename); - for (String loggerName : loggerMap.keySet()) { - LoggerConfig loggerConfig = loggerMap.get(loggerName); - loggerConfig.removeAppender(appender.getName()); - } - loggerContext.updateLoggers(); - fileAppenders.remove(filename); - } - } - - /** - * Set the layout pattern for all {@link ConsoleAppender} and {@link FileAppender} objects. - * - * @param pattern - */ - public void setPattern(String pattern) { - final LoggerContext loggerContext = LoggerContext.getContext(false); - final Configuration config = loggerContext.getConfiguration(); - - layout = PatternLayout.newBuilder().withConfiguration(config).withPattern(pattern).build(); - - Map loggerMap = getLoggerMap(); - for (String loggerName : loggerMap.keySet()) { - LoggerConfig loggerConfig = loggerMap.get(loggerName); - Map appenderMap = loggerConfig.getAppenders(); - for (String appenderName : appenderMap.keySet()) { - Appender newAppender = null; - Appender oldAppender = appenderMap.get(appenderName); - - // there should be a better way to do this - a toBuilder() method on the appender would be - // really useful - if (oldAppender instanceof ConsoleAppender) { - newAppender = ConsoleAppender.newBuilder().setName(appenderName).setConfiguration(config) - .setLayout(layout).build(); - } else if (oldAppender instanceof FileAppender) { - newAppender = FileAppender.newBuilder().setName(appenderName).setConfiguration(config) - .withFileName(((FileAppender) oldAppender).getFileName()).setLayout(layout).build(); + /** + * Get an instance of this singleton class. + * + * @return + */ + public static synchronized Log4j2Configurator getInstance() { + if (instance == null) { + instance = new Log4j2Configurator(); } - if (newAppender != null) { - newAppender.start(); - loggerConfig.removeAppender(appenderName); - loggerConfig.addAppender(newAppender, null, null); - } - } + return instance; } - loggerContext.updateLoggers(); - } - /** - * Sets the levels of parentLogger and all 'child' loggers to the given - * level. This is simply a call to - * - *
-   * Configurator.setAllLevels(parentLogger, level)
-   * 
- * - * @param parentLogger - * @param level - */ - public void setLevel(String parentLogger, Level level) { - Configurator.setAllLevels(parentLogger, level); - } + private Log4j2Configurator() { + final LoggerContext loggerContext = LoggerContext.getContext(false); + final Configuration config = loggerContext.getConfiguration(); + layout = PatternLayout.newBuilder() + .withPattern(DefaultConfiguration.DEFAULT_PATTERN) + .withConfiguration(config) + .build(); + fileAppenders = new HashMap<>(); + setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c{1}:%L] %msg%n%throwable"); + setLevel(Level.INFO); + } - /** - * Set all logger levels. This is simply a call to - * - *
-   * setLevel(LogManager.getRootLogger().getName(), level)
-   * 
- * - * @param level - */ - public void setLevel(Level level) { - setLevel(LogManager.getRootLogger().getName(), level); - } + /** + * + * @return a map of logger names to {@link LoggerConfig} + */ + private Map getLoggerMap() { + final LoggerContext loggerContext = LoggerContext.getContext(false); + final Configuration config = loggerContext.getConfiguration(); + Map loggerMap = new HashMap<>(config.getLoggers()); + loggerMap.put( + LogManager.getRootLogger().getName(), + config.getLoggerConfig(LogManager.getRootLogger().getName())); + return Collections.unmodifiableMap(loggerMap); + } + + /** + * Append log to named file, or create it if it doesn't exist. + * + * @param filename + */ + public void addFile(String filename) { + final LoggerContext loggerContext = LoggerContext.getContext(false); + Map loggerMap = getLoggerMap(); + + FileAppender appender = FileAppender.newBuilder() + .setName(filename) + .withFileName(filename) + .setLayout(layout) + .build(); + appender.start(); + + for (String loggerName : loggerMap.keySet()) { + LoggerConfig loggerConfig = loggerMap.get(loggerName); + loggerConfig.addAppender(appender, null, null); + } + loggerContext.updateLoggers(); + fileAppenders.put(filename, appender); + } + + /** + * Stop logging to named file. + * + * @param filename + */ + public void removeFile(String filename) { + if (fileAppenders.containsKey(filename)) { + final LoggerContext loggerContext = LoggerContext.getContext(false); + Map loggerMap = getLoggerMap(); + + FileAppender appender = fileAppenders.get(filename); + for (String loggerName : loggerMap.keySet()) { + LoggerConfig loggerConfig = loggerMap.get(loggerName); + loggerConfig.removeAppender(appender.getName()); + } + loggerContext.updateLoggers(); + fileAppenders.remove(filename); + } + } + + /** + * Set the layout pattern for all {@link ConsoleAppender} and {@link FileAppender} objects. + * + * @param pattern + */ + public void setPattern(String pattern) { + final LoggerContext loggerContext = LoggerContext.getContext(false); + final Configuration config = loggerContext.getConfiguration(); + + layout = PatternLayout.newBuilder() + .withConfiguration(config) + .withPattern(pattern) + .build(); + + Map loggerMap = getLoggerMap(); + for (String loggerName : loggerMap.keySet()) { + LoggerConfig loggerConfig = loggerMap.get(loggerName); + Map appenderMap = loggerConfig.getAppenders(); + for (String appenderName : appenderMap.keySet()) { + Appender newAppender = null; + Appender oldAppender = appenderMap.get(appenderName); + + // there should be a better way to do this - a toBuilder() method on the appender would be + // really useful + if (oldAppender instanceof ConsoleAppender) { + newAppender = ConsoleAppender.newBuilder() + .setName(appenderName) + .setConfiguration(config) + .setLayout(layout) + .build(); + } else if (oldAppender instanceof FileAppender) { + newAppender = FileAppender.newBuilder() + .setName(appenderName) + .setConfiguration(config) + .withFileName(((FileAppender) oldAppender).getFileName()) + .setLayout(layout) + .build(); + } + if (newAppender != null) { + newAppender.start(); + loggerConfig.removeAppender(appenderName); + loggerConfig.addAppender(newAppender, null, null); + } + } + } + loggerContext.updateLoggers(); + } + + /** + * Sets the levels of parentLogger and all 'child' loggers to the given + * level. This is simply a call to + * + *
+     * Configurator.setAllLevels(parentLogger, level)
+     * 
+ * + * @param parentLogger + * @param level + */ + public void setLevel(String parentLogger, Level level) { + Configurator.setAllLevels(parentLogger, level); + } + + /** + * Set all logger levels. This is simply a call to + * + *
+     * setLevel(LogManager.getRootLogger().getName(), level)
+     * 
+ * + * @param level + */ + public void setLevel(Level level) { + setLevel(LogManager.getRootLogger().getName(), level); + } } diff --git a/src/main/java/terrasaur/utils/NativeLibraryLoader.java b/src/main/java/terrasaur/utils/NativeLibraryLoader.java index 6a8cf73..9a86843 100644 --- a/src/main/java/terrasaur/utils/NativeLibraryLoader.java +++ b/src/main/java/terrasaur/utils/NativeLibraryLoader.java @@ -31,54 +31,53 @@ import vtk.vtkNativeLibrary; /** * Contains a method for loading the VTK native libraries. Any program which makes of VTK classes * must call loadVtkLibraries() beforehand. - * + * * @author kahneg1 * @version 1.0 * */ public class NativeLibraryLoader { - private final static Logger logger = LogManager.getLogger(NativeLibraryLoader.class); + private static final Logger logger = LogManager.getLogger(NativeLibraryLoader.class); - /** load the non-GUI related VTK libraries */ - static public void loadVtkLibraries() { + /** load the non-GUI related VTK libraries */ + public static void loadVtkLibraries() { - List skipLibraries = new ArrayList<>(); - skipLibraries.add("vtkTestingRenderingJava"); - skipLibraries.add("vtkRendering"); - skipLibraries.add("vtkViews"); - skipLibraries.add("vtkInteraction"); - skipLibraries.add("vtkCharts"); - skipLibraries.add("vtkDomainsChemistry"); - skipLibraries.add("vtkIOParallel"); - skipLibraries.add("vtkIOExport"); - skipLibraries.add("vtkIOImport"); - skipLibraries.add("vtkIOMINC"); - skipLibraries.add("vtkFiltersHybrid"); - skipLibraries.add("vtkFiltersParallel"); - skipLibraries.add("vtkGeovis"); + List skipLibraries = new ArrayList<>(); + skipLibraries.add("vtkTestingRenderingJava"); + skipLibraries.add("vtkRendering"); + skipLibraries.add("vtkViews"); + skipLibraries.add("vtkInteraction"); + skipLibraries.add("vtkCharts"); + skipLibraries.add("vtkDomainsChemistry"); + skipLibraries.add("vtkIOParallel"); + skipLibraries.add("vtkIOExport"); + skipLibraries.add("vtkIOImport"); + skipLibraries.add("vtkIOMINC"); + skipLibraries.add("vtkFiltersHybrid"); + skipLibraries.add("vtkFiltersParallel"); + skipLibraries.add("vtkGeovis"); - for (vtkNativeLibrary lib : vtkNativeLibrary.values()) { - try { - boolean loadThis = true; - for (String skipLibrary : skipLibraries) { - if (lib.GetLibraryName().startsWith(skipLibrary)) { - loadThis = false; - break; - } + for (vtkNativeLibrary lib : vtkNativeLibrary.values()) { + try { + boolean loadThis = true; + for (String skipLibrary : skipLibraries) { + if (lib.GetLibraryName().startsWith(skipLibrary)) { + loadThis = false; + break; + } + } + + if (loadThis) lib.LoadLibrary(); + + } catch (UnsatisfiedLinkError e) { + logger.warn(e.getLocalizedMessage()); + } } - - if (loadThis) - lib.LoadLibrary(); - - } catch (UnsatisfiedLinkError e) { - logger.warn(e.getLocalizedMessage()); - } } - } - /** load the JNISpice library */ - static public void loadSpiceLibraries() { - System.loadLibrary("JNISpice"); - } + /** load the JNISpice library */ + public static void loadSpiceLibraries() { + System.loadLibrary("JNISpice"); + } } diff --git a/src/main/java/terrasaur/utils/PhotometricFunction.java b/src/main/java/terrasaur/utils/PhotometricFunction.java index 28b040a..8d9aa75 100644 --- a/src/main/java/terrasaur/utils/PhotometricFunction.java +++ b/src/main/java/terrasaur/utils/PhotometricFunction.java @@ -29,82 +29,76 @@ import java.util.Map; public abstract class PhotometricFunction { - public abstract double getValue(double cosI, double cosE, double phaseDeg); + public abstract double getValue(double cosI, double cosE, double phaseDeg); - public static PhotometricFunction OREX1 = new PhotometricFunction() { - @Override - public double getValue(double cosI, double cosE, double phaseDeg) { - if (cosI < 0 || cosE < 0) - return 0; - double fls = Math.exp(((-0.000000990 * phaseDeg + 0.000269) * phaseDeg - 0.0436) * phaseDeg); - return fls * cosI / (cosI + cosE); + public static PhotometricFunction OREX1 = new PhotometricFunction() { + @Override + public double getValue(double cosI, double cosE, double phaseDeg) { + if (cosI < 0 || cosE < 0) return 0; + double fls = Math.exp(((-0.000000990 * phaseDeg + 0.000269) * phaseDeg - 0.0436) * phaseDeg); + return fls * cosI / (cosI + cosE); + } + }; + + public static PhotometricFunction LommelSeeliger = new PhotometricFunction() { + @Override + public double getValue(double cosI, double cosE, double phaseDeg) { + if (cosI < 0 || cosE < 0) return 0; + return 2 * cosI / (cosI + cosE); + } + }; + + public static PhotometricFunction Lunar = new PhotometricFunction() { + @Override + public double getValue(double cosI, double cosE, double phaseDeg) { + if (cosI < 0 || cosE < 0) return 0; + double phase0 = 60; + double beta = Math.exp(-phaseDeg / phase0); + return (1 - beta) * cosI + 2 * beta * cosI / (cosI + cosE); + } + }; + + public static PhotometricFunction McEwen = new PhotometricFunction() { + @Override + public double getValue(double cosI, double cosE, double phaseDeg) { + if (cosI < 0 || cosE < 0) return 0; + double phase0 = 60; + double beta = Math.exp(-phaseDeg / phase0); + return (1 - beta) * cosI + beta * cosI / (cosI + cosE); + } + }; + + public static PhotometricFunction NoPhase = new PhotometricFunction() { + @Override + public double getValue(double cosI, double cosE, double phaseDeg) { + if (cosI < 0 || cosE < 0) return 0; + double beta = 0.65; + return (1 - beta) * cosI + beta * cosI / (cosI + cosE); + } + }; + + public static Map getFuncMap() { + Map map = new HashMap<>(); + map.put("OREX".toUpperCase(), OREX1); + map.put("LommelSeeliger".toUpperCase(), LommelSeeliger); + map.put("Lunar".toUpperCase(), Lunar); + map.put("McEwen".toUpperCase(), McEwen); + map.put("NoPhase".toUpperCase(), NoPhase); + + return map; } - }; - public static PhotometricFunction LommelSeeliger = new PhotometricFunction() { - @Override - public double getValue(double cosI, double cosE, double phaseDeg) { - if (cosI < 0 || cosE < 0) - return 0; - return 2 * cosI / (cosI + cosE); + public static String getOptionString() { + StringBuffer sb = new StringBuffer(); + sb.append("Photometric function. Supported values are "); + List names = new ArrayList<>(getFuncMap().keySet()); + for (int i = 0; i < names.size() - 1; i++) sb.append(String.format("%s, ", names.get(i))); + sb.append(String.format("or %s.", names.get(names.size() - 1))); + return sb.toString(); } - }; - public static PhotometricFunction Lunar = new PhotometricFunction() { - @Override - public double getValue(double cosI, double cosE, double phaseDeg) { - if (cosI < 0 || cosE < 0) - return 0; - double phase0 = 60; - double beta = Math.exp(-phaseDeg / phase0); - return (1 - beta) * cosI + 2 * beta * cosI / (cosI + cosE); + public static PhotometricFunction getPhotometricFunction(String funcName) { + Map map = getFuncMap(); + return map.get(funcName.toUpperCase()); } - }; - - public static PhotometricFunction McEwen = new PhotometricFunction() { - @Override - public double getValue(double cosI, double cosE, double phaseDeg) { - if (cosI < 0 || cosE < 0) - return 0; - double phase0 = 60; - double beta = Math.exp(-phaseDeg / phase0); - return (1 - beta) * cosI + beta * cosI / (cosI + cosE); - } - }; - - public static PhotometricFunction NoPhase = new PhotometricFunction() { - @Override - public double getValue(double cosI, double cosE, double phaseDeg) { - if (cosI < 0 || cosE < 0) - return 0; - double beta = 0.65; - return (1 - beta) * cosI + beta * cosI / (cosI + cosE); - } - }; - - public static Map getFuncMap() { - Map map = new HashMap<>(); - map.put("OREX".toUpperCase(), OREX1); - map.put("LommelSeeliger".toUpperCase(), LommelSeeliger); - map.put("Lunar".toUpperCase(), Lunar); - map.put("McEwen".toUpperCase(), McEwen); - map.put("NoPhase".toUpperCase(), NoPhase); - - return map; - } - - public static String getOptionString() { - StringBuffer sb = new StringBuffer(); - sb.append("Photometric function. Supported values are "); - List names = new ArrayList<>(getFuncMap().keySet()); - for (int i = 0; i < names.size() - 1; i++) - sb.append(String.format("%s, ", names.get(i))); - sb.append(String.format("or %s.", names.get(names.size() - 1))); - return sb.toString(); - } - - public static PhotometricFunction getPhotometricFunction(String funcName) { - Map map = getFuncMap(); - return map.get(funcName.toUpperCase()); - } } diff --git a/src/main/java/terrasaur/utils/PolyDataStatistics.java b/src/main/java/terrasaur/utils/PolyDataStatistics.java index 034f30b..4782f89 100644 --- a/src/main/java/terrasaur/utils/PolyDataStatistics.java +++ b/src/main/java/terrasaur/utils/PolyDataStatistics.java @@ -40,474 +40,469 @@ import org.apache.commons.text.WordUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import picante.math.vectorspace.VectorIJK; +import spice.basic.LatitudinalCoordinates; +import spice.basic.SpiceException; +import spice.basic.Vector3; import terrasaur.apps.PointCloudToPlane; import terrasaur.apps.ValidateOBJ; import terrasaur.smallBodyModel.BoundingBox; import terrasaur.utils.mesh.TriangularFacet; -import spice.basic.LatitudinalCoordinates; -import spice.basic.SpiceException; -import spice.basic.Vector3; import vtk.vtkIdList; import vtk.vtkPolyData; public class PolyDataStatistics { - private final static Logger logger = LogManager.getLogger(PolyDataStatistics.class); + private static final Logger logger = LogManager.getLogger(PolyDataStatistics.class); - private long numberPlates; - private long numberVertices; - private int numberEdges; - private int enumberDuplicateVertices; - private double surfaceArea; - private double meanCellArea; - private double minCellArea; - private double maxCellArea; - private double stdCellArea; - private double varCellArea; - private double meanEdgeLength; - private double minEdgeLength; - private double maxEdgeLength; - private double stdEdgeLength; - private double varEdgeLength; - private boolean isClosed; - private BoundingBox boundingBox = new BoundingBox(); - private long eulerPolyhedronFormula; - private int unusedVertexCount; - private StringBuilder vertexErrorMessage; - private double[][] inertiaCOM = new double[3][3]; - private double[][] inertiaWorld = new double[3][3]; - private double[] centroid = new double[3]; - private double volume; + private long numberPlates; + private long numberVertices; + private int numberEdges; + private int enumberDuplicateVertices; + private double surfaceArea; + private double meanCellArea; + private double minCellArea; + private double maxCellArea; + private double stdCellArea; + private double varCellArea; + private double meanEdgeLength; + private double minEdgeLength; + private double maxEdgeLength; + private double stdEdgeLength; + private double varEdgeLength; + private boolean isClosed; + private BoundingBox boundingBox = new BoundingBox(); + private long eulerPolyhedronFormula; + private int unusedVertexCount; + private StringBuilder vertexErrorMessage; + private double[][] inertiaCOM = new double[3][3]; + private double[][] inertiaWorld = new double[3][3]; + private double[] centroid = new double[3]; + private double volume; - private ArrayList principalAxes; + private ArrayList principalAxes; - private vtkPolyData polydata; + private vtkPolyData polydata; - public PolyDataStatistics(vtkPolyData polydata) { - this.polydata = polydata; - getPolyDataStatistics(); - } - - private void getPolyDataStatistics() { - - // First determine if the shape model is closed - vtkPolyData boundary = PolyDataUtil.getBoundary(polydata); - isClosed = (boundary.GetNumberOfCells() == 0); - - polydata.BuildCells(); - vtkIdList idList = new vtkIdList(); - - long numberOfCells = polydata.GetNumberOfCells(); - - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; - Set> edges = new LinkedHashSet<>(); - - DescriptiveStatistics areaStatistics = new DescriptiveStatistics(); - for (int i = 0; i < numberOfCells; ++i) { - polydata.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - polydata.GetPoint(id0, pt0); - polydata.GetPoint(id1, pt1); - polydata.GetPoint(id2, pt2); - - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - double area = facet.getArea(); - - areaStatistics.addValue(area); - - edges.add(id0 < id1 ? new Pair<>(id0, id1) : new Pair<>(id1, id0)); - edges.add(id1 < id2 ? new Pair<>(id1, id2) : new Pair<>(id2, id1)); - edges.add(id2 < id0 ? new Pair<>(id2, id0) : new Pair<>(id0, id1)); + public PolyDataStatistics(vtkPolyData polydata) { + this.polydata = polydata; + getPolyDataStatistics(); } - DescriptiveStatistics edgeStatistics = new DescriptiveStatistics(); - for (Pair edge : edges) { - polydata.GetPoint(edge.getKey(), pt0); - polydata.GetPoint(edge.getValue(), pt1); - double length = new Vector3D(pt0).distance(new Vector3D(pt1)); - edgeStatistics.addValue(length); + private void getPolyDataStatistics() { + + // First determine if the shape model is closed + vtkPolyData boundary = PolyDataUtil.getBoundary(polydata); + isClosed = (boundary.GetNumberOfCells() == 0); + + polydata.BuildCells(); + vtkIdList idList = new vtkIdList(); + + long numberOfCells = polydata.GetNumberOfCells(); + + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; + Set> edges = new LinkedHashSet<>(); + + DescriptiveStatistics areaStatistics = new DescriptiveStatistics(); + for (int i = 0; i < numberOfCells; ++i) { + polydata.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + polydata.GetPoint(id0, pt0); + polydata.GetPoint(id1, pt1); + polydata.GetPoint(id2, pt2); + + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + double area = facet.getArea(); + + areaStatistics.addValue(area); + + edges.add(id0 < id1 ? new Pair<>(id0, id1) : new Pair<>(id1, id0)); + edges.add(id1 < id2 ? new Pair<>(id1, id2) : new Pair<>(id2, id1)); + edges.add(id2 < id0 ? new Pair<>(id2, id0) : new Pair<>(id0, id1)); + } + + DescriptiveStatistics edgeStatistics = new DescriptiveStatistics(); + for (Pair edge : edges) { + polydata.GetPoint(edge.getKey(), pt0); + polydata.GetPoint(edge.getValue(), pt1); + double length = new Vector3D(pt0).distance(new Vector3D(pt1)); + edgeStatistics.addValue(length); + } + + if (isClosed()) { + volume = getMassProperties(); + + // For debugging, print out this information to make sure it agrees + // with our implementation + /*- + vtkMassProperties massProp = new vtkMassProperties(); + massProp.SetInputData(polydata); + massProp.Update(); + + System.out.println("Surface Area = " + + massProp.GetSurfaceArea()); + System.out.println("Volume = " + massProp.GetVolume()); + System.out.println("Mean Plate Area = " + + massProp.GetSurfaceArea() / polydata.GetNumberOfCells()); + System.out.println("Min Plate Area = " + + massProp.GetMinCellArea()); + System.out.println("Max Plate Area = " + + massProp.GetMaxCellArea()); + */ + } + + eulerPolyhedronFormula = polydata.GetNumberOfPoints() - edges.size() + numberOfCells; + if (isClosed() && eulerPolyhedronFormula != 2) { + vertexErrorMessage = new StringBuilder(); + vertexErrorMessage.append("Warning: The polyhedron is closed, but the Euler polyhedron formula, V-E+F, " + + "(see https://en.wikipedia.org/wiki/Euler_characteristic) does not equal 2, " + + "as would be expected. This is usually caused by duplicate vertices in the " + + "shape model. Please contact the creator of the shape model to see if this " + + "can be corrected."); + } + + polydata.ComputeBounds(); + boundingBox = new BoundingBox(polydata.GetBounds()); + + numberPlates = numberOfCells; + numberVertices = polydata.GetNumberOfPoints(); + numberEdges = edges.size(); + surfaceArea = areaStatistics.getSum(); + meanCellArea = areaStatistics.getMean(); + minCellArea = areaStatistics.getMin(); + maxCellArea = areaStatistics.getMax(); + stdCellArea = areaStatistics.getStandardDeviation(); + varCellArea = areaStatistics.getVariance(); + meanEdgeLength = edgeStatistics.getMean(); + minEdgeLength = edgeStatistics.getMin(); + maxEdgeLength = edgeStatistics.getMax(); + stdEdgeLength = edgeStatistics.getStandardDeviation(); + varEdgeLength = edgeStatistics.getVariance(); } - if (isClosed()) { - volume = getMassProperties(); + private List evaluateOpenModel() throws SpiceException { + NativeLibraryLoader.loadSpiceLibraries(); - // For debugging, print out this information to make sure it agrees - // with our implementation - /*- - vtkMassProperties massProp = new vtkMassProperties(); - massProp.SetInputData(polydata); - massProp.Update(); - - System.out.println("Surface Area = " + - massProp.GetSurfaceArea()); - System.out.println("Volume = " + massProp.GetVolume()); - System.out.println("Mean Plate Area = " + - massProp.GetSurfaceArea() / polydata.GetNumberOfCells()); - System.out.println("Min Plate Area = " + - massProp.GetMinCellArea()); - System.out.println("Max Plate Area = " + - massProp.GetMaxCellArea()); - */ + PointCloudToPlane pctp = new PointCloudToPlane(polydata.GetPoints()); + List globalPoints = new ArrayList<>(); + VectorStatistics vStats = new VectorStatistics(); + double[] pt = new double[3]; + for (int i = 0; i < polydata.GetNumberOfPoints(); i++) { + polydata.GetPoint(i, pt); + Vector3 v = new Vector3(pt); + globalPoints.add(v); + vStats.add(v); + } + + List localPoints = pctp.getGMU().globalToLocal(globalPoints); + NavigableMap cornerMap = new TreeMap<>(); + for (int i = 0; i < 4; i++) cornerMap.put(i, new Vector3()); + for (Vector3 localPoint : localPoints) { + double x = localPoint.getElt(0); + double y = localPoint.getElt(1); + int quadrant; + if (x < 0) quadrant = (y > 0 ? 1 : 2); + else quadrant = (y > 0 ? 0 : 3); + + Vector3 v = cornerMap.get(quadrant); + if (localPoint.norm() > v.norm()) cornerMap.put(quadrant, localPoint); + } + + List localCorners = new ArrayList<>(cornerMap.values()); + List globalCorners = pctp.getGMU().localToGlobal(localCorners); + + List list = new ArrayList<>(); + + Vector3D center = vStats.getMean(); + double lon = Math.toDegrees(center.getAlpha()); + if (lon < 0) lon += 360; + list.add(String.format("%-26s = %f", "Center Longitude (deg)", lon)); + list.add(String.format("%-26s = %f", "Center Latitude (deg)", Math.toDegrees(center.getDelta()))); + for (int i = 0; i < globalCorners.size(); i++) { + LatitudinalCoordinates lc = new LatitudinalCoordinates(globalCorners.get(i)); + lon = Math.toDegrees(lc.getLongitude()); + if (lon < 0) lon += 360; + list.add(String.format("%-26s = %f", "Corner " + i + " Longitude (deg)", lon)); + list.add(String.format("%-26s = %f", "Corner " + i + " Latitude (deg)", Math.toDegrees(lc.getLatitude()))); + } + + return list; } - eulerPolyhedronFormula = polydata.GetNumberOfPoints() - edges.size() + numberOfCells; - if (isClosed() && eulerPolyhedronFormula != 2) { - vertexErrorMessage = new StringBuilder(); - vertexErrorMessage - .append("Warning: The polyhedron is closed, but the Euler polyhedron formula, V-E+F, " - + "(see https://en.wikipedia.org/wiki/Euler_characteristic) does not equal 2, " - + "as would be expected. This is usually caused by duplicate vertices in the " - + "shape model. Please contact the creator of the shape model to see if this " - + "can be corrected."); + /** + * The following function was adapted from from the file Wm5PolyhedralMassProperties.cpp in the + * Geometric Tools source code (http://www.geometrictools.com) + * + * @return mass + */ + private double getMassProperties() { + long numberOfCells = polydata.GetNumberOfCells(); + vtkIdList idList = new vtkIdList(); + + double[] v0 = new double[3]; + double[] v1 = new double[3]; + double[] v2 = new double[3]; + + final double oneDiv6 = 1.0 / 6.0; + final double oneDiv24 = 1.0 / 24.0; + final double oneDiv60 = 1.0 / 60.0; + final double oneDiv120 = 1.0 / 120.0; + + // order: 1, x, y, z, x^2, y^2, z^2, xy, yz, zx + double[] integral = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + + for (int i = 0; i < numberOfCells; ++i) { + polydata.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + polydata.GetPoint(id0, v0); + polydata.GetPoint(id1, v1); + polydata.GetPoint(id2, v2); + + // Get cross product of edges and normal vector. + Vector3D V1mV0 = new Vector3D(v1).subtract(new Vector3D(v0)); + Vector3D V2mV0 = new Vector3D(v2).subtract(new Vector3D(v0)); + double[] N = V1mV0.crossProduct(V2mV0).toArray(); + // Vector3 V1mV0 = v1 - v0; + // Vector3 V2mV0 = v2 - v0; + // Vector3 N = V1mV0.Cross(V2mV0); + + // Compute integral terms. + double tmp0, tmp1, tmp2; + double f1x, f2x, f3x, g0x, g1x, g2x; + tmp0 = v0[0] + v1[0]; + f1x = tmp0 + v2[0]; + tmp1 = v0[0] * v0[0]; + tmp2 = tmp1 + v1[0] * tmp0; + f2x = tmp2 + v2[0] * f1x; + f3x = v0[0] * tmp1 + v1[0] * tmp2 + v2[0] * f2x; + g0x = f2x + v0[0] * (f1x + v0[0]); + g1x = f2x + v1[0] * (f1x + v1[0]); + g2x = f2x + v2[0] * (f1x + v2[0]); + + double f1y, f2y, f3y, g0y, g1y, g2y; + tmp0 = v0[1] + v1[1]; + f1y = tmp0 + v2[1]; + tmp1 = v0[1] * v0[1]; + tmp2 = tmp1 + v1[1] * tmp0; + f2y = tmp2 + v2[1] * f1y; + f3y = v0[1] * tmp1 + v1[1] * tmp2 + v2[1] * f2y; + g0y = f2y + v0[1] * (f1y + v0[1]); + g1y = f2y + v1[1] * (f1y + v1[1]); + g2y = f2y + v2[1] * (f1y + v2[1]); + + double f1z, f2z, f3z, g0z, g1z, g2z; + tmp0 = v0[2] + v1[2]; + f1z = tmp0 + v2[2]; + tmp1 = v0[2] * v0[2]; + tmp2 = tmp1 + v1[2] * tmp0; + f2z = tmp2 + v2[2] * f1z; + f3z = v0[2] * tmp1 + v1[2] * tmp2 + v2[2] * f2z; + g0z = f2z + v0[2] * (f1z + v0[2]); + g1z = f2z + v1[2] * (f1z + v1[2]); + g2z = f2z + v2[2] * (f1z + v2[2]); + + // Update integrals. + integral[0] += N[0] * f1x; + integral[1] += N[0] * f2x; + integral[2] += N[1] * f2y; + integral[3] += N[2] * f2z; + integral[4] += N[0] * f3x; + integral[5] += N[1] * f3y; + integral[6] += N[2] * f3z; + integral[7] += N[0] * (v0[1] * g0x + v1[1] * g1x + v2[1] * g2x); + integral[8] += N[1] * (v0[2] * g0y + v1[2] * g1y + v2[2] * g2y); + integral[9] += N[2] * (v0[0] * g0z + v1[0] * g1z + v2[0] * g2z); + } + + integral[0] *= oneDiv6; + integral[1] *= oneDiv24; + integral[2] *= oneDiv24; + integral[3] *= oneDiv24; + integral[4] *= oneDiv60; + integral[5] *= oneDiv60; + integral[6] *= oneDiv60; + integral[7] *= oneDiv120; + integral[8] *= oneDiv120; + integral[9] *= oneDiv120; + + // mass + double mass = integral[0]; + + // center of mass + centroid[0] = integral[1] / mass; + centroid[1] = integral[2] / mass; + centroid[2] = integral[3] / mass; + + // inertia relative to world origin + inertiaWorld[0][0] = integral[5] + integral[6]; + inertiaWorld[0][1] = -integral[7]; + inertiaWorld[0][2] = -integral[9]; + inertiaWorld[1][0] = inertiaWorld[0][1]; + inertiaWorld[1][1] = integral[4] + integral[6]; + inertiaWorld[1][2] = -integral[8]; + inertiaWorld[2][0] = inertiaWorld[0][2]; + inertiaWorld[2][1] = inertiaWorld[1][2]; + inertiaWorld[2][2] = integral[4] + integral[5]; + + // inertia relative to center of mass + for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) inertiaCOM[i][j] = inertiaWorld[i][j]; + inertiaCOM[0][0] -= mass * (centroid[1] * centroid[1] + centroid[2] * centroid[2]); + inertiaCOM[0][1] += mass * centroid[0] * centroid[1]; + inertiaCOM[0][2] += mass * centroid[2] * centroid[0]; + inertiaCOM[1][0] = inertiaCOM[0][1]; + inertiaCOM[1][1] -= mass * (centroid[2] * centroid[2] + centroid[0] * centroid[0]); + inertiaCOM[1][2] += mass * centroid[1] * centroid[2]; + inertiaCOM[2][0] = inertiaCOM[0][2]; + inertiaCOM[2][1] = inertiaCOM[1][2]; + inertiaCOM[2][2] -= mass * (centroid[0] * centroid[0] + centroid[1] * centroid[1]); + + RealMatrix inertiaTensor = new Array2DRowRealMatrix(inertiaWorld); + EigenDecomposition ed = new EigenDecomposition(inertiaTensor); + principalAxes = new ArrayList<>(); + for (int i = 0; i < 3; i++) principalAxes.add(ed.getEigenvector(i).toArray()); + + return mass; } - polydata.ComputeBounds(); - boundingBox = new BoundingBox(polydata.GetBounds()); + public Map getShapeModelStatsMap() { + Map stats = new TreeMap<>(); - numberPlates = numberOfCells; - numberVertices = polydata.GetNumberOfPoints(); - numberEdges = edges.size(); - surfaceArea = areaStatistics.getSum(); - meanCellArea = areaStatistics.getMean(); - minCellArea = areaStatistics.getMin(); - maxCellArea = areaStatistics.getMax(); - stdCellArea = areaStatistics.getStandardDeviation(); - varCellArea = areaStatistics.getVariance(); - meanEdgeLength = edgeStatistics.getMean(); - minEdgeLength = edgeStatistics.getMin(); - maxEdgeLength = edgeStatistics.getMax(); - stdEdgeLength = edgeStatistics.getStandardDeviation(); - varEdgeLength = edgeStatistics.getVariance(); - - } - - private List evaluateOpenModel() throws SpiceException { - NativeLibraryLoader.loadSpiceLibraries(); - - PointCloudToPlane pctp = new PointCloudToPlane(polydata.GetPoints()); - List globalPoints = new ArrayList<>(); - VectorStatistics vStats = new VectorStatistics(); - double[] pt = new double[3]; - for (int i = 0; i < polydata.GetNumberOfPoints(); i++) { - polydata.GetPoint(i, pt); - Vector3 v = new Vector3(pt); - globalPoints.add(v); - vStats.add(v); + stats.put("Number of Plates", Long.toString(numberPlates)); + stats.put("Number of Vertices", Long.toString(numberVertices)); + stats.put("Number of Edges", Integer.toString(numberEdges)); + stats.put("Euler Polyhedron Formula", Long.toString(eulerPolyhedronFormula)); + stats.put("Surface Area", String.format("%-21.16g km^2", surfaceArea)); + stats.put("Plate Area Mean", String.format("%-21.16g km^2", meanCellArea)); + stats.put("Plate Area Min", String.format("%-21.16g km^2", minCellArea)); + stats.put("Plate Area Standard Dev", String.format("%-21.16g km^2", stdCellArea)); + stats.put("Edge Length Mean", String.format("%-21.16g km", meanEdgeLength)); + stats.put("Edge Length Max", String.format("%-21.16g km", maxEdgeLength)); + stats.put("Edge Length Variance", String.format("%-21.16g km", varEdgeLength)); + stats.put("Surface Closed?", String.format("%s", isClosed ? "Yes" : "No")); + if (isClosed()) { + stats.put("Volume", String.format("%-21.16g km^3", getVolume())); + stats.put("Centroid", Arrays.toString(centroid) + " km"); + stats.put( + "Moment of Inertia Tensor Relative To Origin", + Arrays.toString(inertiaWorld[0]) + " " + Arrays.toString(inertiaWorld[1]) + " " + + Arrays.toString(inertiaWorld[2])); + stats.put( + "Moment of Inertia Tensor Relative To Centroid", + Arrays.toString(inertiaCOM[0]) + " " + Arrays.toString(inertiaCOM[1]) + " " + + Arrays.toString(inertiaCOM[2])); + for (int i = 0; i < principalAxes.size(); i++) { + stats.put("Principal Axis " + i, Arrays.toString(principalAxes.get(i))); + } + } + stats.put( + "Extent X", + "[" + boundingBox.getXRange().getBegin() + ", " + + boundingBox.getXRange().getEnd() + "] km"); + stats.put( + "Extent Y", + "[" + boundingBox.getYRange().getBegin() + ", " + + boundingBox.getYRange().getEnd() + "] km"); + stats.put( + "Extent Z", + "[" + boundingBox.getZRange().getBegin() + ", " + + boundingBox.getZRange().getEnd() + "] km"); + if (isClosed() && eulerPolyhedronFormula != 2) { + logger.warn(vertexErrorMessage.toString()); + } + return stats; } - List localPoints = pctp.getGMU().globalToLocal(globalPoints); - NavigableMap cornerMap = new TreeMap<>(); - for (int i = 0; i < 4; i++) - cornerMap.put(i, new Vector3()); - for (Vector3 localPoint : localPoints) { - double x = localPoint.getElt(0); - double y = localPoint.getElt(1); - int quadrant; - if (x < 0) - quadrant = (y > 0 ? 1 : 2); - else - quadrant = (y > 0 ? 0 : 3); + /** + * return an array of strings to be used to add comments to an OBJ file as defined in the SIS. + * + * @return + * @throws Exception + */ + public ArrayList getShapeModelStats() throws Exception { + ArrayList stats = new ArrayList(); - Vector3 v = cornerMap.get(quadrant); - if (localPoint.norm() > v.norm()) - cornerMap.put(quadrant, localPoint); + stats.add(String.format("%-26s = %d", "Number of Plates", numberPlates)); + stats.add(String.format("%-26s = %d", "Number of Vertices", numberVertices)); + stats.add(String.format("%-26s = %d", "Number of Edges", numberEdges)); + stats.add(String.format("%-26s = %d", "Euler Polyhedron Formula", eulerPolyhedronFormula)); + stats.add(String.format("%-26s = %-21.16g km^2", "Surface Area", surfaceArea)); + stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Mean", meanCellArea)); + stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Min", minCellArea)); + stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Standard Dev", stdCellArea)); + stats.add(String.format("%-26s = %-21.16g km", "Edge Length Mean", meanEdgeLength)); + stats.add(String.format("%-26s = %-21.16g km", "Edge Length Max", maxEdgeLength)); + stats.add(String.format("%-26s = %-21.16g km^2", "Edge Length Variance", varEdgeLength)); + stats.add(String.format("%-26s = %s", "Surface Closed? ", isClosed ? "Yes" : "No")); + if (isClosed()) { + stats.add(String.format("%-26s = %-21.16g km^3", "Volume", getVolume())); + stats.add("Centroid [km]:"); + stats.add(" " + Arrays.toString(centroid)); + stats.add("Moment of Inertia Tensor Relative To Origin [kg km^2]:"); + stats.add(" " + Arrays.toString(inertiaWorld[0])); + stats.add(" " + Arrays.toString(inertiaWorld[1])); + stats.add(" " + Arrays.toString(inertiaWorld[2])); + stats.add("Moment of Inertia Tensor Relative To Centroid [kg km^2]:"); + stats.add(" " + Arrays.toString(inertiaCOM[0])); + stats.add(" " + Arrays.toString(inertiaCOM[1])); + stats.add(" " + Arrays.toString(inertiaCOM[2])); + for (int i = 0; i < principalAxes.size(); i++) { + stats.add("Principal Axis " + i); + stats.add(" " + Arrays.toString(principalAxes.get(i))); + } + } else { + stats.addAll(evaluateOpenModel()); + } + stats.add("Extent [km]:"); + stats.add(" X: [" + boundingBox.getXRange().getBegin() + ", " + + boundingBox.getXRange().getEnd() + "]"); + stats.add(" Y: [" + boundingBox.getYRange().getBegin() + ", " + + boundingBox.getYRange().getEnd() + "]"); + stats.add(" Z: [" + boundingBox.getZRange().getBegin() + ", " + + boundingBox.getZRange().getEnd() + "]"); + if (isClosed() && eulerPolyhedronFormula != 2) { + for (String s : WordUtils.wrap(vertexErrorMessage.toString(), 80, "\n", true) + .split("\n")) stats.add(s); + } + + ValidateOBJ vo = new ValidateOBJ(polydata); + if (isClosed()) { + vo.testFacets(); + stats.add(vo.getMessage()); + vo.testVertices(); + stats.add(vo.getMessage()); + } + vo.findDuplicateVertices(); + stats.add(vo.getMessage()); + vo.findUnreferencedVertices(); + stats.add(vo.getMessage()); + vo.findZeroAreaFacets(); + stats.add(vo.getMessage()); + + return stats; } - List localCorners = new ArrayList<>(cornerMap.values()); - List globalCorners = pctp.getGMU().localToGlobal(localCorners); - - List list = new ArrayList<>(); - - Vector3D center = vStats.getMean(); - double lon = Math.toDegrees(center.getAlpha()); - if (lon < 0) - lon += 360; - list.add(String.format("%-26s = %f", "Center Longitude (deg)", lon)); - list.add( - String.format("%-26s = %f", "Center Latitude (deg)", Math.toDegrees(center.getDelta()))); - for (int i = 0; i < globalCorners.size(); i++) { - LatitudinalCoordinates lc = new LatitudinalCoordinates(globalCorners.get(i)); - lon = Math.toDegrees(lc.getLongitude()); - if (lon < 0) - lon += 360; - list.add(String.format("%-26s = %f", "Corner " + i + " Longitude (deg)", lon)); - list.add(String.format("%-26s = %f", "Corner " + i + " Latitude (deg)", - Math.toDegrees(lc.getLatitude()))); + public double[] getCentroid() { + return centroid; } - return list; - } - - /** - * The following function was adapted from from the file Wm5PolyhedralMassProperties.cpp in the - * Geometric Tools source code (http://www.geometrictools.com) - * - * @return mass - */ - private double getMassProperties() { - long numberOfCells = polydata.GetNumberOfCells(); - vtkIdList idList = new vtkIdList(); - - double[] v0 = new double[3]; - double[] v1 = new double[3]; - double[] v2 = new double[3]; - - final double oneDiv6 = 1.0 / 6.0; - final double oneDiv24 = 1.0 / 24.0; - final double oneDiv60 = 1.0 / 60.0; - final double oneDiv120 = 1.0 / 120.0; - - // order: 1, x, y, z, x^2, y^2, z^2, xy, yz, zx - double[] integral = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - - for (int i = 0; i < numberOfCells; ++i) { - polydata.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - polydata.GetPoint(id0, v0); - polydata.GetPoint(id1, v1); - polydata.GetPoint(id2, v2); - - // Get cross product of edges and normal vector. - Vector3D V1mV0 = new Vector3D(v1).subtract(new Vector3D(v0)); - Vector3D V2mV0 = new Vector3D(v2).subtract(new Vector3D(v0)); - double[] N = V1mV0.crossProduct(V2mV0).toArray(); - // Vector3 V1mV0 = v1 - v0; - // Vector3 V2mV0 = v2 - v0; - // Vector3 N = V1mV0.Cross(V2mV0); - - // Compute integral terms. - double tmp0, tmp1, tmp2; - double f1x, f2x, f3x, g0x, g1x, g2x; - tmp0 = v0[0] + v1[0]; - f1x = tmp0 + v2[0]; - tmp1 = v0[0] * v0[0]; - tmp2 = tmp1 + v1[0] * tmp0; - f2x = tmp2 + v2[0] * f1x; - f3x = v0[0] * tmp1 + v1[0] * tmp2 + v2[0] * f2x; - g0x = f2x + v0[0] * (f1x + v0[0]); - g1x = f2x + v1[0] * (f1x + v1[0]); - g2x = f2x + v2[0] * (f1x + v2[0]); - - double f1y, f2y, f3y, g0y, g1y, g2y; - tmp0 = v0[1] + v1[1]; - f1y = tmp0 + v2[1]; - tmp1 = v0[1] * v0[1]; - tmp2 = tmp1 + v1[1] * tmp0; - f2y = tmp2 + v2[1] * f1y; - f3y = v0[1] * tmp1 + v1[1] * tmp2 + v2[1] * f2y; - g0y = f2y + v0[1] * (f1y + v0[1]); - g1y = f2y + v1[1] * (f1y + v1[1]); - g2y = f2y + v2[1] * (f1y + v2[1]); - - double f1z, f2z, f3z, g0z, g1z, g2z; - tmp0 = v0[2] + v1[2]; - f1z = tmp0 + v2[2]; - tmp1 = v0[2] * v0[2]; - tmp2 = tmp1 + v1[2] * tmp0; - f2z = tmp2 + v2[2] * f1z; - f3z = v0[2] * tmp1 + v1[2] * tmp2 + v2[2] * f2z; - g0z = f2z + v0[2] * (f1z + v0[2]); - g1z = f2z + v1[2] * (f1z + v1[2]); - g2z = f2z + v2[2] * (f1z + v2[2]); - - // Update integrals. - integral[0] += N[0] * f1x; - integral[1] += N[0] * f2x; - integral[2] += N[1] * f2y; - integral[3] += N[2] * f2z; - integral[4] += N[0] * f3x; - integral[5] += N[1] * f3y; - integral[6] += N[2] * f3z; - integral[7] += N[0] * (v0[1] * g0x + v1[1] * g1x + v2[1] * g2x); - integral[8] += N[1] * (v0[2] * g0y + v1[2] * g1y + v2[2] * g2y); - integral[9] += N[2] * (v0[0] * g0z + v1[0] * g1z + v2[0] * g2z); + public double getMeanEdgeLength() { + return meanEdgeLength; } - integral[0] *= oneDiv6; - integral[1] *= oneDiv24; - integral[2] *= oneDiv24; - integral[3] *= oneDiv24; - integral[4] *= oneDiv60; - integral[5] *= oneDiv60; - integral[6] *= oneDiv60; - integral[7] *= oneDiv120; - integral[8] *= oneDiv120; - integral[9] *= oneDiv120; - - // mass - double mass = integral[0]; - - // center of mass - centroid[0] = integral[1] / mass; - centroid[1] = integral[2] / mass; - centroid[2] = integral[3] / mass; - - // inertia relative to world origin - inertiaWorld[0][0] = integral[5] + integral[6]; - inertiaWorld[0][1] = -integral[7]; - inertiaWorld[0][2] = -integral[9]; - inertiaWorld[1][0] = inertiaWorld[0][1]; - inertiaWorld[1][1] = integral[4] + integral[6]; - inertiaWorld[1][2] = -integral[8]; - inertiaWorld[2][0] = inertiaWorld[0][2]; - inertiaWorld[2][1] = inertiaWorld[1][2]; - inertiaWorld[2][2] = integral[4] + integral[5]; - - // inertia relative to center of mass - for (int i = 0; i < 3; ++i) - for (int j = 0; j < 3; ++j) - inertiaCOM[i][j] = inertiaWorld[i][j]; - inertiaCOM[0][0] -= mass * (centroid[1] * centroid[1] + centroid[2] * centroid[2]); - inertiaCOM[0][1] += mass * centroid[0] * centroid[1]; - inertiaCOM[0][2] += mass * centroid[2] * centroid[0]; - inertiaCOM[1][0] = inertiaCOM[0][1]; - inertiaCOM[1][1] -= mass * (centroid[2] * centroid[2] + centroid[0] * centroid[0]); - inertiaCOM[1][2] += mass * centroid[1] * centroid[2]; - inertiaCOM[2][0] = inertiaCOM[0][2]; - inertiaCOM[2][1] = inertiaCOM[1][2]; - inertiaCOM[2][2] -= mass * (centroid[0] * centroid[0] + centroid[1] * centroid[1]); - - RealMatrix inertiaTensor = new Array2DRowRealMatrix(inertiaWorld); - EigenDecomposition ed = new EigenDecomposition(inertiaTensor); - principalAxes = new ArrayList<>(); - for (int i = 0; i < 3; i++) - principalAxes.add(ed.getEigenvector(i).toArray()); - - return mass; - } - - public Map getShapeModelStatsMap() { - Map stats = new TreeMap<>(); - - stats.put("Number of Plates", Long.toString(numberPlates)); - stats.put("Number of Vertices", Long.toString(numberVertices)); - stats.put("Number of Edges", Integer.toString(numberEdges)); - stats.put("Euler Polyhedron Formula", Long.toString(eulerPolyhedronFormula)); - stats.put("Surface Area", String.format("%-21.16g km^2", surfaceArea)); - stats.put("Plate Area Mean", String.format("%-21.16g km^2", meanCellArea)); - stats.put("Plate Area Min", String.format("%-21.16g km^2", minCellArea)); - stats.put("Plate Area Standard Dev", String.format("%-21.16g km^2", stdCellArea)); - stats.put("Edge Length Mean", String.format("%-21.16g km", meanEdgeLength)); - stats.put("Edge Length Max", String.format("%-21.16g km", maxEdgeLength)); - stats.put("Edge Length Variance", String.format("%-21.16g km", varEdgeLength)); - stats.put("Surface Closed?", String.format("%s", isClosed ? "Yes" : "No")); - if (isClosed()) { - stats.put("Volume", String.format("%-21.16g km^3", getVolume())); - stats.put("Centroid", Arrays.toString(centroid) + " km"); - stats.put("Moment of Inertia Tensor Relative To Origin", Arrays.toString(inertiaWorld[0]) - + " " + Arrays.toString(inertiaWorld[1]) + " " + Arrays.toString(inertiaWorld[2])); - stats.put("Moment of Inertia Tensor Relative To Centroid", Arrays.toString(inertiaCOM[0]) - + " " + Arrays.toString(inertiaCOM[1]) + " " + Arrays.toString(inertiaCOM[2])); - for (int i = 0; i < principalAxes.size(); i++) { - stats.put("Principal Axis " + i, Arrays.toString(principalAxes.get(i))); - } - } - stats.put("Extent X", "[" + boundingBox.getXRange().getBegin() + ", " - + boundingBox.getXRange().getEnd() + "] km"); - stats.put("Extent Y", "[" + boundingBox.getYRange().getBegin() + ", " - + boundingBox.getYRange().getEnd() + "] km"); - stats.put("Extent Z", "[" + boundingBox.getZRange().getBegin() + ", " - + boundingBox.getZRange().getEnd() + "] km"); - if (isClosed() && eulerPolyhedronFormula != 2) { - logger.warn(vertexErrorMessage.toString()); - } - return stats; - } - - /** - * return an array of strings to be used to add comments to an OBJ file as defined in the SIS. - * - * @return - * @throws Exception - */ - public ArrayList getShapeModelStats() throws Exception { - ArrayList stats = new ArrayList(); - - stats.add(String.format("%-26s = %d", "Number of Plates", numberPlates)); - stats.add(String.format("%-26s = %d", "Number of Vertices", numberVertices)); - stats.add(String.format("%-26s = %d", "Number of Edges", numberEdges)); - stats.add(String.format("%-26s = %d", "Euler Polyhedron Formula", eulerPolyhedronFormula)); - stats.add(String.format("%-26s = %-21.16g km^2", "Surface Area", surfaceArea)); - stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Mean", meanCellArea)); - stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Min", minCellArea)); - stats.add(String.format("%-26s = %-21.16g km^2", "Plate Area Standard Dev", stdCellArea)); - stats.add(String.format("%-26s = %-21.16g km", "Edge Length Mean", meanEdgeLength)); - stats.add(String.format("%-26s = %-21.16g km", "Edge Length Max", maxEdgeLength)); - stats.add(String.format("%-26s = %-21.16g km^2", "Edge Length Variance", varEdgeLength)); - stats.add(String.format("%-26s = %s", "Surface Closed? ", isClosed ? "Yes" : "No")); - if (isClosed()) { - stats.add(String.format("%-26s = %-21.16g km^3", "Volume", getVolume())); - stats.add("Centroid [km]:"); - stats.add(" " + Arrays.toString(centroid)); - stats.add("Moment of Inertia Tensor Relative To Origin [kg km^2]:"); - stats.add(" " + Arrays.toString(inertiaWorld[0])); - stats.add(" " + Arrays.toString(inertiaWorld[1])); - stats.add(" " + Arrays.toString(inertiaWorld[2])); - stats.add("Moment of Inertia Tensor Relative To Centroid [kg km^2]:"); - stats.add(" " + Arrays.toString(inertiaCOM[0])); - stats.add(" " + Arrays.toString(inertiaCOM[1])); - stats.add(" " + Arrays.toString(inertiaCOM[2])); - for (int i = 0; i < principalAxes.size(); i++) { - stats.add("Principal Axis " + i); - stats.add(" " + Arrays.toString(principalAxes.get(i))); - } - } else { - stats.addAll(evaluateOpenModel()); - } - stats.add("Extent [km]:"); - stats.add(" X: [" + boundingBox.getXRange().getBegin() + ", " - + boundingBox.getXRange().getEnd() + "]"); - stats.add(" Y: [" + boundingBox.getYRange().getBegin() + ", " - + boundingBox.getYRange().getEnd() + "]"); - stats.add(" Z: [" + boundingBox.getZRange().getBegin() + ", " - + boundingBox.getZRange().getEnd() + "]"); - if (isClosed() && eulerPolyhedronFormula != 2) { - for (String s : WordUtils.wrap(vertexErrorMessage.toString(), 80, "\n", true).split("\n")) - stats.add(s); + public ArrayList getPrincipalAxes() { + return principalAxes; } - ValidateOBJ vo = new ValidateOBJ(polydata); - if (isClosed()) { - vo.testFacets(); - stats.add(vo.getMessage()); - vo.testVertices(); - stats.add(vo.getMessage()); + public double getVolume() { + return volume; } - vo.findDuplicateVertices(); - stats.add(vo.getMessage()); - vo.findUnreferencedVertices(); - stats.add(vo.getMessage()); - vo.findZeroAreaFacets(); - stats.add(vo.getMessage()); - return stats; - - } - - public double[] getCentroid() { - return centroid; - } - - public double getMeanEdgeLength() { - return meanEdgeLength; - } - - public ArrayList getPrincipalAxes() { - return principalAxes; - } - - public double getVolume() { - return volume; - } - - public boolean isClosed() { - return isClosed; - } + public boolean isClosed() { + return isClosed; + } } diff --git a/src/main/java/terrasaur/utils/PolyDataUtil.java b/src/main/java/terrasaur/utils/PolyDataUtil.java index 8071490..7c4c9d9 100644 --- a/src/main/java/terrasaur/utils/PolyDataUtil.java +++ b/src/main/java/terrasaur/utils/PolyDataUtil.java @@ -34,7 +34,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; @@ -45,7 +44,10 @@ import java.util.NavigableMap; import java.util.NavigableSet; import java.util.TreeMap; import java.util.TreeSet; - +import nom.tam.fits.BasicHDU; +import nom.tam.fits.Fits; +import nom.tam.fits.Header; +import nom.tam.fits.HeaderCard; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.math3.geometry.euclidean.threed.Rotation; @@ -56,20 +58,16 @@ import org.apache.commons.math3.linear.SingularValueDecomposition; import org.apache.commons.math3.util.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import nom.tam.fits.BasicHDU; -import nom.tam.fits.Fits; -import nom.tam.fits.Header; -import nom.tam.fits.HeaderCard; import picante.math.coords.CoordConverters; import picante.math.coords.LatitudinalVector; -import picante.math.vectorspace.VectorIJK; import picante.math.vectorspace.UnwritableVectorIJK; -import terrasaur.utils.math.MathConversions; -import terrasaur.utils.math.RotationUtils; -import terrasaur.utils.mesh.TriangularFacet; +import picante.math.vectorspace.VectorIJK; import spice.basic.Plane; import spice.basic.SpiceException; import spice.basic.Vector3; +import terrasaur.utils.math.MathConversions; +import terrasaur.utils.math.RotationUtils; +import terrasaur.utils.mesh.TriangularFacet; import vtk.vtkAbstractPointLocator; import vtk.vtkCellArray; import vtk.vtkDataArray; @@ -95,1750 +93,1725 @@ import vtk.vtksbCellLocator; /** Utilities for working with a {@link vtkPolyData} object */ public class PolyDataUtil { - private static final Logger logger = LogManager.getLogger(); - - public static final float INVALID_VALUE = -1.0e38f; - - /** add point normals if this polydata does not already contain them */ - public static void addPointNormalsToShapeModel(vtkPolyData polyData) { - if (polyData.GetPointData().GetNormals() == null) { - // Add normal vectors - vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); - normalsFilter.SetInputData(polyData); - normalsFilter.SetComputeCellNormals(0); - normalsFilter.SetComputePointNormals(1); - normalsFilter.SplittingOff(); - normalsFilter.AutoOrientNormalsOn(); - normalsFilter.ConsistencyOn(); - normalsFilter.Update(); - - vtkPolyData normalsOutput = normalsFilter.GetOutput(); - polyData.ShallowCopy(normalsOutput); - - normalsFilter.Delete(); - } - } - - /** - * Compute the mean normal vector over the entire vtkPolyData by averaging all the normal vectors - * of all cells. - */ - public static Vector3D computeMeanPolyDataNormal(vtkPolyData polyData) { - - // Average the normals - double[] normal = {0.0, 0.0, 0.0}; - - long numCells = polyData.GetNumberOfCells(); - for (int i = 0; i < numCells; ++i) { - TriangularFacet tf = getFacet(polyData, i); - UnwritableVectorIJK n = tf.getNormal(); - normal[0] += n.getI(); - normal[1] += n.getJ(); - normal[2] += n.getK(); - } - - normal[0] /= numCells; - normal[1] /= numCells; - normal[2] /= numCells; - - return new Vector3D(normal); - } - - /** - * @return the centroid of all the points in the polydata. - */ - public static Vector3D computePolyDataCentroid(vtkPolyData polyData) { - // Average the normals - double[] centroid = {0.0, 0.0, 0.0}; - - long numPoints = polyData.GetNumberOfPoints(); - double[] p = new double[3]; - for (int i = 0; i < numPoints; ++i) { - polyData.GetPoint(i, p); - centroid[0] += p[0]; - centroid[1] += p[1]; - centroid[2] += p[2]; - } - - centroid[0] /= numPoints; - centroid[1] /= numPoints; - centroid[2] /= numPoints; - - return new Vector3D(centroid); - } - - /** - * Reduce the number of cells in a mesh by targetReduction. For example, if the mesh contains 100 - * triangles and targetReduction is .90, after the decimation there will be approximately 10 - * triangles - a 90% reduction. - * - * @param polydata - * @param targetReduction fraction between zero and one - */ - public static void decimatePolyData(vtkPolyData polydata, double targetReduction) { - vtkDecimatePro dec = new vtkDecimatePro(); - dec.SetInputData(polydata); - dec.SetTargetReduction(targetReduction); - dec.PreserveTopologyOn(); - dec.SplittingOff(); - dec.BoundaryVertexDeletionOff(); - dec.SetMaximumError(Double.MAX_VALUE); - dec.AccumulateErrorOn(); - dec.PreSplitMeshOn(); - dec.Update(); - vtkPolyData decOutput = dec.GetOutput(); - - polydata.DeepCopy(decOutput); - - dec.Delete(); - } - - /** - * Fit a {@link Plane} to a polyData. - * - * @param polyData - * @return - */ - public static Plane fitPlaneToPolyData(vtkPolyData polyData) { - Pair entry = findLocalFrame(polyData); - Vector3 normal = MathConversions.toVector3(entry.getKey().applyInverseTo(Vector3D.PLUS_K)); - Vector3 pointInPlane = MathConversions.toVector3(entry.getValue()); - Plane p = null; - try { - p = new Plane(normal, pointInPlane); - } catch (SpiceException e) { - logger.warn(e.getLocalizedMessage()); - } - return p; - } - - /** - * Return a rotation matrix where the Z axis points along the average normal or radial, and the - * centroid of all of the points. This is only meaningful with local shape models. - * - * @return - */ - public static Pair findLocalFrame(vtkPolyData polyData) { - try { - Vector3D centroid = computePolyDataCentroid(polyData); - - // subtract out the centroid from the points - int numPoints = (int) polyData.GetNumberOfPoints(); - double[][] points = new double[3][numPoints]; - double[] p = new double[3]; - for (int i = 0; i < numPoints; ++i) { - polyData.GetPoint(i, p); - points[0][i] = p[0] - centroid.getX(); - points[1][i] = p[1] - centroid.getY(); - points[2][i] = p[2] - centroid.getZ(); - } - RealMatrix pointMatrix = new Array2DRowRealMatrix(points, false); - - // Now do SVD on this matrix - SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); - RealMatrix u = svd.getU(); - - // uz points normal to the plane and equals the eigenvector - // corresponding to the smallest eigenvalue of the V matrix - Vector3D uz = new Vector3D(u.getColumn(2)).normalize(); - - // Make sure uz points away from the asteroid rather than towards it - // by looking at the dot product of uz and a point normal to the - // data. If dot product is negative, reverse uz. - Vector3D normal = computeMeanPolyDataNormal(polyData); - - normal = normal.normalize(); - if (normal.dotProduct(uz) < 0) uz = uz.scalarMultiply(-1); - - // make ux and uy perpendicular to uz - Vector3D ux = uz.crossProduct(Vector3D.PLUS_K); - if (ux.getNorm() == 0) ux = Vector3D.PLUS_I; - - Rotation rot = RotationUtils.KprimaryIsecondary(uz, ux); - return new Pair(rot, centroid); - } catch (Exception e) { - logger.warn(e.getLocalizedMessage()); - return null; - } - } - - public static double[] getPolyDataNormalAtPoint( - double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator) { - vtkIdList idList = new vtkIdList(); - - pointLocator.FindClosestNPoints(20, pt, idList); - - // Average the normals - double[] normal = {0.0, 0.0, 0.0}; - - long N = idList.GetNumberOfIds(); - if (N < 1) return null; - - vtkDataArray normals = polyData.GetPointData().GetNormals(); - for (int i = 0; i < N; ++i) { - double[] tmp = normals.GetTuple3(idList.GetId(i)); - normal[0] += tmp[0]; - normal[1] += tmp[1]; - normal[2] += tmp[2]; - } - - normal[0] /= N; - normal[1] /= N; - normal[2] /= N; - - idList.Delete(); - - return normal; - } - - public static double[] getPolyDataNormalAtPointWithinRadius( - double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator, double radius) { - vtkIdList idList = new vtkIdList(); - - pointLocator.FindPointsWithinRadius(radius, pt, idList); - - // Average the normals - double[] normal = {0.0, 0.0, 0.0}; - - long N = idList.GetNumberOfIds(); - if (N < 1) return null; - - vtkDataArray normals = polyData.GetPointData().GetNormals(); - for (int i = 0; i < N; ++i) { - double[] tmp = normals.GetTuple3(idList.GetId(i)); - normal[0] += tmp[0]; - normal[1] += tmp[1]; - normal[2] += tmp[2]; - } - - normal[0] /= N; - normal[1] /= N; - normal[2] /= N; - - idList.Delete(); - - return normal; - } - - /** - * @param polydata - */ - public static vtkPolyData getBoundary(vtkPolyData polydata) { - // Compute the bounding edges of this surface - vtkFeatureEdges edgeExtracter = new vtkFeatureEdges(); - edgeExtracter.SetInputData(polydata); - edgeExtracter.BoundaryEdgesOn(); - edgeExtracter.FeatureEdgesOff(); - edgeExtracter.NonManifoldEdgesOff(); - edgeExtracter.ManifoldEdgesOff(); - edgeExtracter.Update(); - - vtkPolyData edgeExtracterOutput = edgeExtracter.GetOutput(); - - vtkPolyData boundary = new vtkPolyData(); - boundary.DeepCopy(edgeExtracterOutput); - - edgeExtracter.Delete(); - return boundary; - } - - /** - * @param polyData shape model - * @param i facet index - * @return {@link TriangularFacet} with index i - */ - public static TriangularFacet getFacet(vtkPolyData polyData, long i) { - vtkIdList idList = new vtkIdList(); - polyData.GetCellPoints(i, idList); - - double[] pt = new double[3]; - polyData.GetPoint(idList.GetId(0), pt); - VectorIJK v1 = new VectorIJK(pt); - polyData.GetPoint(idList.GetId(1), pt); - VectorIJK v2 = new VectorIJK(pt); - polyData.GetPoint(idList.GetId(2), pt); - VectorIJK v3 = new VectorIJK(pt); - - return new TriangularFacet(v1, v2, v3); - } - - /** - * Return the vtkIDList for points found within radius of point pt. - * - * @param pt - * @param polyData - * @param pointLocator - * @param radius - * @return - */ - public static vtkIdList getIDsAtPointWithinRadius( - double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator, double radius) { - - vtkIdList idList = new vtkIdList(); - pointLocator.FindPointsWithinRadius(radius, pt, idList); - return idList; - } - - /** - * A 3D Delaunay triangulation is constructed from the points in region0. Any points from region1 - * contained within this mesh are considered to be in the overlapping region. - * - * @param region0 Reference point set - * @param region1 Test point set - * @return points in region1 contained within region0 - */ - public static vtkUnstructuredGrid intersectingPoints(vtkPoints region0, vtkPoints region1) { - vtkUnstructuredGrid region0Grid = new vtkUnstructuredGrid(); - region0Grid.SetPoints(region0); - - vtkDelaunay3D vtkd = new vtkDelaunay3D(); - vtkd.SetInputData(region0Grid); - vtkd.Update(); - - vtkGeometryFilter vtkgf = new vtkGeometryFilter(); - vtkgf.SetInputData(vtkd.GetOutput()); - vtkgf.Update(); - - vtksbCellLocator cellLoc = new vtksbCellLocator(); - cellLoc.SetDataSet(vtkgf.GetOutput()); - cellLoc.BuildLocator(); - - // vtkPolyDataWriter pdw = new vtkPolyDataWriter(); - // pdw.SetInputData(vtkgf.GetOutput()); - // pdw.SetFileName("./fixedHull.vtk"); - // pdw.SetFileTypeToBinary(); - // pdw.Update(); - - double[] origin = {0., 0., 0.}; - double tol = 1e-6; - double[] t = new double[1]; - double[] x = new double[3]; - double[] pcoords = new double[3]; - int[] subId = new int[1]; - long[] cellId = new long[1]; - vtkGenericCell genericCell = new vtkGenericCell(); - vtkPoints intersected = new vtkPoints(); - for (int i = 0; i < region1.GetNumberOfPoints(); i++) { - double[] p = region1.GetPoint(i); - double[] endPt = new double[3]; - for (int j = 0; j < 3; j++) endPt[j] = p[j] * 10; - int code = - cellLoc.IntersectWithLine(origin, endPt, tol, t, x, pcoords, subId, cellId, genericCell); - if (code == 0) continue; - intersected.InsertNextPoint(p); - } - - // System.out.printf("intersectingPoints: %d intersecting points\n", - // intersected.GetNumberOfPoints()); - - vtkUnstructuredGrid returnGrid = new vtkUnstructuredGrid(); - returnGrid.SetPoints(intersected); - - return returnGrid; - } - - /** - * Read in a shape model in ICQ format and convert to PLT format. - * - * @param icqFile - * @return - */ - private static List icq2plt(String icqFile) { - try { - List lines = FileUtils.readLines(new File(icqFile), Charset.defaultCharset()); - return icq2plt(lines); - } catch (IOException e) { - logger.error(e.getLocalizedMessage()); - return null; - } - } - - /** - * Read in a shape model in ICQ format and convert to PLT format. Based on SHAPE2PLATES from SPC. - * - * @param icqLines - * @return - */ - private static List icq2plt(List icqLines) { - - List pltLines = new ArrayList<>(); - - String[] parts = icqLines.get(0).strip().split("\\s+"); - int q = Integer.parseInt(parts[0].strip()); - - pltLines.add(String.format("%d", (int) (6 * Math.pow(q + 1, 2)))); - - double[][][][] vec = new double[3][q + 1][q + 1][6]; - int[][][] n = new int[q + 1][q + 1][6]; - - int n0 = 0; - for (int f = 0; f < 6; f++) { - for (int j = 0; j <= q; j++) { - for (int i = 0; i <= q; i++) { - parts = icqLines.get(++n0).strip().split("\\s+"); - for (int k = 0; k < 3; k++) - vec[k][i][j][f] = Double.parseDouble(parts[k].strip().replaceAll("D", "E")); - pltLines.add( - String.format( - "%10d %.12f %.12f %.12f", n0, vec[0][i][j][f], vec[1][i][j][f], vec[2][i][j][f])); - n[i][j][f] = n0; + private static final Logger logger = LogManager.getLogger(); + + public static final float INVALID_VALUE = -1.0e38f; + + /** add point normals if this polydata does not already contain them */ + public static void addPointNormalsToShapeModel(vtkPolyData polyData) { + if (polyData.GetPointData().GetNormals() == null) { + // Add normal vectors + vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); + normalsFilter.SetInputData(polyData); + normalsFilter.SetComputeCellNormals(0); + normalsFilter.SetComputePointNormals(1); + normalsFilter.SplittingOff(); + normalsFilter.AutoOrientNormalsOn(); + normalsFilter.ConsistencyOn(); + normalsFilter.Update(); + + vtkPolyData normalsOutput = normalsFilter.GetOutput(); + polyData.ShallowCopy(normalsOutput); + + normalsFilter.Delete(); } - } } - for (int i = 1; i < q; i++) { - n[i][q][5] = n[q - i][q][3]; - n[i][0][5] = n[i][q][1]; - n[i][0][4] = n[q][q - i][0]; - n[i][0][3] = n[q - i][0][0]; - n[i][0][2] = n[0][i][0]; - n[i][0][1] = n[i][q][0]; - } + /** + * Compute the mean normal vector over the entire vtkPolyData by averaging all the normal vectors + * of all cells. + */ + public static Vector3D computeMeanPolyDataNormal(vtkPolyData polyData) { - for (int j = 1; j < q; j++) { - n[q][j][5] = n[j][q][4]; - n[q][j][4] = n[0][j][3]; - n[q][j][3] = n[0][j][2]; - n[q][j][2] = n[0][j][1]; - n[0][j][5] = n[q - j][q][2]; - n[0][j][4] = n[q][j][1]; - } + // Average the normals + double[] normal = {0.0, 0.0, 0.0}; - n[0][0][2] = n[0][0][0]; - n[q][0][3] = n[0][0][0]; - n[0][0][1] = n[0][q][0]; - n[q][0][2] = n[0][q][0]; - n[0][0][3] = n[q][0][0]; - n[q][0][4] = n[q][0][0]; - n[0][0][4] = n[q][q][0]; - n[q][0][1] = n[q][q][0]; - n[0][0][5] = n[0][q][1]; - n[q][q][2] = n[0][q][1]; - n[0][q][4] = n[q][q][1]; - n[q][0][5] = n[q][q][1]; - n[q][q][3] = n[0][q][2]; - n[0][q][5] = n[0][q][2]; - 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()); -// } - - pltLines.add(String.format("%d", 12 * q * q)); - n0 = 0; - for (int f = 0; f < 6; f++) { - for (int i = 0; i < q; i++) { - for (int j = 0; j < q; j++) { - - // @formatter:off - double w1x = - vec[1][i][j][f] * vec[2][i + 1][j + 1][f] - vec[2][i][j][f] * vec[1][i + 1][j + 1][f]; - double w1y = - vec[2][i][j][f] * vec[0][i + 1][j + 1][f] - vec[0][i][j][f] * vec[2][i + 1][j + 1][f]; - double w1z = - vec[0][i][j][f] * vec[1][i + 1][j + 1][f] - vec[1][i][j][f] * vec[0][i + 1][j + 1][f]; - double w2x = - vec[1][i + 1][j][f] * vec[2][i][j + 1][f] - vec[2][i + 1][j][f] * vec[1][i][j + 1][f]; - double w2y = - vec[2][i + 1][j][f] * vec[0][i][j + 1][f] - vec[0][i + 1][j][f] * vec[2][i][j + 1][f]; - double w2z = - vec[0][i + 1][j][f] * vec[1][i][j + 1][f] - vec[1][i + 1][j][f] * vec[0][i][j + 1][f]; - // @formatter:on - - double z1 = w1x * w1x + w1y * w1y + w1z * w1z; - double z2 = w2x * w2x + w2y * w2y + w2z * w2z; - - if (z1 <= z2) { - - n0++; - pltLines.add( - String.format( - "%10d %10d %10d %10d", n0, n[i][j][f], n[i + 1][j + 1][f], n[i + 1][j][f])); - - n0++; - pltLines.add( - String.format( - "%10d %10d %10d %10d", n0, n[i][j][f], n[i][j + 1][f], n[i + 1][j + 1][f])); - } else { - n0++; - pltLines.add( - String.format( - "%10d %10d %10d %10d", n0, n[i][j][f], n[i][j + 1][f], n[i + 1][j][f])); - - n0++; - pltLines.add( - String.format( - "%10d %10d %10d %10d", n0, n[i + 1][j][f], n[i][j + 1][f], n[i + 1][j + 1][f])); - } + long numCells = polyData.GetNumberOfCells(); + for (int i = 0; i < numCells; ++i) { + TriangularFacet tf = getFacet(polyData, i); + UnwritableVectorIJK n = tf.getNormal(); + normal[0] += n.getI(); + normal[1] += n.getJ(); + normal[2] += n.getK(); } - } + + normal[0] /= numCells; + normal[1] /= numCells; + normal[2] /= numCells; + + return new Vector3D(normal); } - return pltLines; - } + /** + * @return the centroid of all the points in the polydata. + */ + public static Vector3D computePolyDataCentroid(vtkPolyData polyData) { + // Average the normals + double[] centroid = {0.0, 0.0, 0.0}; - /** - * Read in PDS vertex file format. There are 2 variants of this file. In one the first line - * contains the number of points and the number of cells and then follows the points and vertices. - * In the other variant the first line only contains the number of points, then follows the - * points, then follows a line listing the number of cells followed by the cells. Support both - * variants here. - * - * @param lines - * @return - */ - private static vtkPolyData loadPDSShapeModel(List lines) { - vtkPolyData polydata = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray cells = new vtkCellArray(); - polydata.SetPoints(points); - polydata.SetPolys(cells); - - // Read in the first line which list the number of points and plates - int count = 0; - String val = lines.get(count++).strip(); - String[] vals = val.split("\\s+"); - int numPoints = -1; - int numCells = -1; - if (vals.length == 1) { - numPoints = Integer.parseInt(vals[0]); - } else if (vals.length == 2) { - numPoints = Integer.parseInt(vals[0]); - numCells = Integer.parseInt(vals[1]); - } else { - logger.error("Invalid format"); - return polydata; - } - - for (int j = 0; j < numPoints; ++j) { - vals = lines.get(count++).strip().split("\\s+"); - double x = Double.parseDouble(vals[1]); - double y = Double.parseDouble(vals[2]); - double z = Double.parseDouble(vals[3]); - points.InsertNextPoint(x, y, z); - } - - if (numCells == -1) { - val = lines.get(count++).strip(); - numCells = Integer.parseInt(val); - } - - vtkIdList idList = new vtkIdList(); - idList.SetNumberOfIds(3); - for (int j = 0; j < numCells; ++j) { - vals = lines.get(count++).strip().split("\\s+"); - int idx1 = Integer.parseInt(vals[1]) - 1; - int idx2 = Integer.parseInt(vals[2]) - 1; - int idx3 = Integer.parseInt(vals[3]) - 1; - idList.SetId(0, idx1); - idList.SetId(1, idx2); - idList.SetId(2, idx3); - cells.InsertNextCell(idList); - } - - idList.Delete(); - - return polydata; - } - - /** - * Read in PDS vertex file format. There are 2 variants of this file. In one the first line - * contains the number of points and the number of cells and then follows the points and vertices. - * In the other variant the first line only contains the number of points, then follows the - * points, then follows a line listing the number of cells followed by the cells. Support both - * variants here. - * - * @param filename - * @return - * @throws IOException - */ - private static vtkPolyData loadPDSShapeModel(String filename) { - try { - List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); - return loadPDSShapeModel(lines); - } catch (IOException e) { - logger.error(e.getLocalizedMessage()); - return null; - } - } - - /** - * Several PDS shape models are in special format similar to standard Gaskell vertex shape models - * but are zero based and don't have a first column listing the id. - * - * @param filename - * @param inMeters If true, vertices are assumed to be in meters. If false, assumed to be - * kilometers. - * @return - * @throws IOException - */ - private static vtkPolyData loadTempel1AndWild2ShapeModel(String filename, boolean inMeters) - throws Exception { - vtkPolyData polydata = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray cells = new vtkCellArray(); - polydata.SetPoints(points); - polydata.SetPolys(cells); - - InputStream fs = new FileInputStream(filename); - InputStreamReader isr = new InputStreamReader(fs); - BufferedReader in = new BufferedReader(isr); - - // Read in the first line which lists the number of points and plates - String val = in.readLine().trim(); - String[] vals = val.split("\\s+"); - int numPoints = -1; - int numCells = -1; - if (vals.length == 2) { - numPoints = Integer.parseInt(vals[0]); - numCells = Integer.parseInt(vals[1]); - } else { - in.close(); - throw new IOException("Format not valid"); - } - - for (int j = 0; j < numPoints; ++j) { - vals = in.readLine().trim().split("\\s+"); - double x = Double.parseDouble(vals[0]); - double y = Double.parseDouble(vals[1]); - double z = Double.parseDouble(vals[2]); - - if (inMeters) { - x /= 1000.0; - y /= 1000.0; - z /= 1000.0; - } - - points.InsertNextPoint(x, y, z); - } - - vtkIdList idList = new vtkIdList(); - idList.SetNumberOfIds(3); - for (int j = 0; j < numCells; ++j) { - vals = in.readLine().trim().split("\\s+"); - int idx1 = Integer.parseInt(vals[0]); - int idx2 = Integer.parseInt(vals[1]); - int idx3 = Integer.parseInt(vals[2]); - idList.SetId(0, idx1); - idList.SetId(1, idx2); - idList.SetId(2, idx3); - cells.InsertNextCell(idList); - } - - idList.Delete(); - - in.close(); - - return polydata; - } - - public static vtkPolyData loadLocalFitsLLRModelN(double[][][] data) throws Exception { - return loadLocalFitsLLRModelN(data, null, null, 0); - } - - /** - * Read in a FITS local shape model (i.e. a surface patch such as a maplet) with format where the - * first 6 planes are lat, lon, radius, x, y, z and return the result as a vtkPolyData. Note only - * the x, y, z planes are read to get the point data. The lat, lon, radius planes are ignored. - * - *

The user must also define the number of planes to skip before parsing the ancillary data and - * storing it in List<vtkFloatArray>. - * - *

For example, if planesToSkip = 6 then any planes after the 6th plane will be stored and - * returned in ancillaryData. The user may decide to set planesToSkip = 0 in which case ALL the - * planes in the fits file will be returned in ancillaryData, including the first 6 planes (lat, - * lon, radius, x, y, z). - * - * @param data - * @param ancillaryData - list of vtkFloatArray found by parsing planes in the fits file. NOTE: - * Any prior content in ancillaryData will be cleared by the method and repopulated with - * results from the fits file! - * @param planeNames - * @param planesToSkip - integer number of the planes to skip before putting planes in - * ancillaryData - * @return - * @throws Exception - */ - public static vtkPolyData loadLocalFitsLLRModelN( - double[][][] data, - List ancillaryData, - List planeNames, - int planesToSkip) - throws Exception { - int xIndex = 3; - int yIndex = 4; - int zIndex = 5; - - vtkIdList idList = new vtkIdList(); - vtkPolyData dem = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - dem.SetPoints(points); - dem.SetPolys(polys); - - int numExtraPlanes = data.length - planesToSkip; - if (ancillaryData != null) { - ancillaryData.clear(); - for (int i = 0; i < numExtraPlanes; ++i) { - vtkFloatArray array = new vtkFloatArray(); - array.SetName(planeNames.get(i + planesToSkip)); - ancillaryData.add(array); - } - } - - int liveSizeX = data[0].length; - int liveSizeY = data[0][0].length; - - int[][] indices = new int[liveSizeX][liveSizeY]; - int c = 0; - int i0, i1, i2, i3; - - // First add points to the vtkPoints array - for (int m = 0; m < liveSizeX; ++m) - for (int n = 0; n < liveSizeY; ++n) { - indices[m][n] = -1; - - double x = data[xIndex][m][n]; - double y = data[yIndex][m][n]; - double z = data[zIndex][m][n]; - - boolean valid = (x != INVALID_VALUE && y != INVALID_VALUE && z != INVALID_VALUE); - - if (valid) { - - double[] pt = {x, y, z}; - points.InsertNextPoint(pt); - - if (ancillaryData != null) { - for (int k = 0; k < numExtraPlanes; ++k) - ancillaryData.get(k).InsertNextTuple1(data[planesToSkip + k][m][n]); - } - - indices[m][n] = c; - - ++c; + long numPoints = polyData.GetNumberOfPoints(); + double[] p = new double[3]; + for (int i = 0; i < numPoints; ++i) { + polyData.GetPoint(i, p); + centroid[0] += p[0]; + centroid[1] += p[1]; + centroid[2] += p[2]; } - } - idList.SetNumberOfIds(3); + centroid[0] /= numPoints; + centroid[1] /= numPoints; + centroid[2] /= numPoints; - // Now add connectivity information - for (int m = 1; m < liveSizeX; ++m) - for (int n = 1; n < liveSizeY; ++n) { - // Get the indices of the 4 corners of the rectangle to the - // upper left - i0 = indices[m - 1][n - 1]; - i1 = indices[m][n - 1]; - i2 = indices[m - 1][n]; - i3 = indices[m][n]; - - // Add upper left triangle - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); - polys.InsertNextCell(idList); - } - // Add bottom right triangle - if (i2 >= 0 && i1 >= 0 && i3 >= 0) { - idList.SetId(0, i2); - idList.SetId(1, i1); - idList.SetId(2, i3); - polys.InsertNextCell(idList); - } - } - - vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); - normalsFilter.SetInputData(dem); - normalsFilter.SetComputeCellNormals(0); - normalsFilter.SetComputePointNormals(1); - normalsFilter.SplittingOff(); - normalsFilter.ConsistencyOn(); - normalsFilter.AutoOrientNormalsOff(); - if (needToFlipMapletNormalVectors(dem)) normalsFilter.FlipNormalsOn(); - else normalsFilter.FlipNormalsOff(); - normalsFilter.Update(); - - vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); - dem.DeepCopy(normalsFilterOutput); - - return dem; - } - - /** - * Read in a shape model with format where each line in file consists of lat, lon, and radius, or - * lon, lat, and radius. Note that most of the shape models of Thomas and Stooke in this format - * use west longtude. The only exception is Thomas's Ida model which uses east longitude. - * - * @param filename - * @param westLongitude if true, assume longitude is west, if false assume east - * @return - * @throws Exception - */ - private static vtkPolyData loadLLRShapeModel(String filename, boolean westLongitude) - throws Exception { - // We need to load the file in 2 passes. In the first pass - // we figure out the latitude/longitude spacing (both assumed same), - // which column is latitude, and which column is longitude. - // - // It is assumed the following: - // If 0 is the first field of the first column, - // then longitude is the first column. - // If -90 is the first field of the first column, - // then latitude is the first column. - // If 90 is the first field of the first column, - // then latitude is the first column. - // - // These assumptions ensure that the shape models of Thomas, Stooke, and Hudson - // are loaded in correctly. However, other shape models in some other lat, lon - // scheme may not be loaded correctly with this function. - // - // In the second pass, we load the file using the values - // determined in the first pass. - - // First pass - double latLonSpacing = 0.0; - int latIndex = 0; - int lonIndex = 1; - - InputStream fs = new FileInputStream(filename); - InputStreamReader isr = new InputStreamReader(fs); - BufferedReader in = new BufferedReader(isr); - - { - // We only need to look at the first 2 lines of the file - // in the first pass to determine everything we need. - String[] vals = in.readLine().trim().split("\\s+"); - double a1 = Double.parseDouble(vals[0]); - double b1 = Double.parseDouble(vals[1]); - vals = in.readLine().trim().split("\\s+"); - double a2 = Double.parseDouble(vals[0]); - double b2 = Double.parseDouble(vals[1]); - - if (a1 == 0.0) { - latIndex = 1; - lonIndex = 0; - } else if (a1 == -90.0 || a1 == 90.0) { - latIndex = 0; - lonIndex = 1; - } else { - System.err.println("loadLLRShapeModel: Incorrect format for input file"); - } - - if (a1 != a2) latLonSpacing = Math.abs(a2 - a1); - else if (b1 != b2) latLonSpacing = Math.abs(b2 - b1); - else System.err.println("loadLLRShapeModel: Incorrect format for input file"); - - in.close(); + return new Vector3D(centroid); } - // Second pass - fs = new FileInputStream(filename); - isr = new InputStreamReader(fs); - in = new BufferedReader(isr); + /** + * Reduce the number of cells in a mesh by targetReduction. For example, if the mesh contains 100 + * triangles and targetReduction is .90, after the decimation there will be approximately 10 + * triangles - a 90% reduction. + * + * @param polydata + * @param targetReduction fraction between zero and one + */ + public static void decimatePolyData(vtkPolyData polydata, double targetReduction) { + vtkDecimatePro dec = new vtkDecimatePro(); + dec.SetInputData(polydata); + dec.SetTargetReduction(targetReduction); + dec.PreserveTopologyOn(); + dec.SplittingOff(); + dec.BoundaryVertexDeletionOff(); + dec.SetMaximumError(Double.MAX_VALUE); + dec.AccumulateErrorOn(); + dec.PreSplitMeshOn(); + dec.Update(); + vtkPolyData decOutput = dec.GetOutput(); - vtkPolyData body = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - body.SetPoints(points); - body.SetPolys(polys); + polydata.DeepCopy(decOutput); - int numRows = (int) Math.round(180.0 / latLonSpacing) + 1; - int numCols = (int) Math.round(360.0 / latLonSpacing) + 1; - - int count = 0; - int[][] indices = new int[numRows][numCols]; - String line; - while ((line = in.readLine()) != null) { - String[] vals = line.trim().split("\\s+"); - double lat = Double.parseDouble(vals[latIndex]); - double lon = Double.parseDouble(vals[lonIndex]); - double rad = Double.parseDouble(vals[2]); - - int row = (int) Math.round((lat + 90.0) / latLonSpacing); - int col = (int) Math.round(lon / latLonSpacing); - - // Only include 1 point at each pole and don't include any points - // at longitude 360 since it's the same as longitude 0 - if ((lat == -90.0 && lon > 0.0) || (lat == 90.0 && lon > 0.0) || lon == 360.0) { - indices[row][col] = -1; - } else { - if (westLongitude) lon = -lon; - - indices[row][col] = count++; - UnwritableVectorIJK v = - CoordConverters.convert( - new LatitudinalVector(rad, Math.toRadians(lat), Math.toRadians(lon))); - double[] pt = {v.getI(), v.getJ(), v.getK()}; - points.InsertNextPoint(pt); - } + dec.Delete(); } - in.close(); + /** + * Fit a {@link Plane} to a polyData. + * + * @param polyData + * @return + */ + public static Plane fitPlaneToPolyData(vtkPolyData polyData) { + Pair entry = findLocalFrame(polyData); + Vector3 normal = MathConversions.toVector3(entry.getKey().applyInverseTo(Vector3D.PLUS_K)); + Vector3 pointInPlane = MathConversions.toVector3(entry.getValue()); + Plane p = null; + try { + p = new Plane(normal, pointInPlane); + } catch (SpiceException e) { + logger.warn(e.getLocalizedMessage()); + } + return p; + } - // Now add connectivity information - int i0, i1, i2, i3; - vtkIdList idList = new vtkIdList(); - idList.SetNumberOfIds(3); - for (int m = 0; m <= numRows - 2; ++m) - for (int n = 0; n <= numCols - 2; ++n) { - // Add triangles touching south pole - if (m == 0) { - i0 = indices[m][0]; // index of south pole point - i1 = indices[m + 1][n]; - if (n == numCols - 2) i2 = indices[m + 1][0]; - else i2 = indices[m + 1][n + 1]; + /** + * Return a rotation matrix where the Z axis points along the average normal or radial, and the + * centroid of all of the points. This is only meaningful with local shape models. + * + * @return + */ + public static Pair findLocalFrame(vtkPolyData polyData) { + try { + Vector3D centroid = computePolyDataCentroid(polyData); - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); + // subtract out the centroid from the points + int numPoints = (int) polyData.GetNumberOfPoints(); + double[][] points = new double[3][numPoints]; + double[] p = new double[3]; + for (int i = 0; i < numPoints; ++i) { + polyData.GetPoint(i, p); + points[0][i] = p[0] - centroid.getX(); + points[1][i] = p[1] - centroid.getY(); + points[2][i] = p[2] - centroid.getZ(); + } + RealMatrix pointMatrix = new Array2DRowRealMatrix(points, false); + + // Now do SVD on this matrix + SingularValueDecomposition svd = new SingularValueDecomposition(pointMatrix); + RealMatrix u = svd.getU(); + + // uz points normal to the plane and equals the eigenvector + // corresponding to the smallest eigenvalue of the V matrix + Vector3D uz = new Vector3D(u.getColumn(2)).normalize(); + + // Make sure uz points away from the asteroid rather than towards it + // by looking at the dot product of uz and a point normal to the + // data. If dot product is negative, reverse uz. + Vector3D normal = computeMeanPolyDataNormal(polyData); + + normal = normal.normalize(); + if (normal.dotProduct(uz) < 0) uz = uz.scalarMultiply(-1); + + // make ux and uy perpendicular to uz + Vector3D ux = uz.crossProduct(Vector3D.PLUS_K); + if (ux.getNorm() == 0) ux = Vector3D.PLUS_I; + + Rotation rot = RotationUtils.KprimaryIsecondary(uz, ux); + return new Pair(rot, centroid); + } catch (Exception e) { + logger.warn(e.getLocalizedMessage()); + return null; + } + } + + public static double[] getPolyDataNormalAtPoint( + double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator) { + vtkIdList idList = new vtkIdList(); + + pointLocator.FindClosestNPoints(20, pt, idList); + + // Average the normals + double[] normal = {0.0, 0.0, 0.0}; + + long N = idList.GetNumberOfIds(); + if (N < 1) return null; + + vtkDataArray normals = polyData.GetPointData().GetNormals(); + for (int i = 0; i < N; ++i) { + double[] tmp = normals.GetTuple3(idList.GetId(i)); + normal[0] += tmp[0]; + normal[1] += tmp[1]; + normal[2] += tmp[2]; + } + + normal[0] /= N; + normal[1] /= N; + normal[2] /= N; + + idList.Delete(); + + return normal; + } + + public static double[] getPolyDataNormalAtPointWithinRadius( + double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator, double radius) { + vtkIdList idList = new vtkIdList(); + + pointLocator.FindPointsWithinRadius(radius, pt, idList); + + // Average the normals + double[] normal = {0.0, 0.0, 0.0}; + + long N = idList.GetNumberOfIds(); + if (N < 1) return null; + + vtkDataArray normals = polyData.GetPointData().GetNormals(); + for (int i = 0; i < N; ++i) { + double[] tmp = normals.GetTuple3(idList.GetId(i)); + normal[0] += tmp[0]; + normal[1] += tmp[1]; + normal[2] += tmp[2]; + } + + normal[0] /= N; + normal[1] /= N; + normal[2] /= N; + + idList.Delete(); + + return normal; + } + + /** + * @param polydata + */ + public static vtkPolyData getBoundary(vtkPolyData polydata) { + // Compute the bounding edges of this surface + vtkFeatureEdges edgeExtracter = new vtkFeatureEdges(); + edgeExtracter.SetInputData(polydata); + edgeExtracter.BoundaryEdgesOn(); + edgeExtracter.FeatureEdgesOff(); + edgeExtracter.NonManifoldEdgesOff(); + edgeExtracter.ManifoldEdgesOff(); + edgeExtracter.Update(); + + vtkPolyData edgeExtracterOutput = edgeExtracter.GetOutput(); + + vtkPolyData boundary = new vtkPolyData(); + boundary.DeepCopy(edgeExtracterOutput); + + edgeExtracter.Delete(); + return boundary; + } + + /** + * @param polyData shape model + * @param i facet index + * @return {@link TriangularFacet} with index i + */ + public static TriangularFacet getFacet(vtkPolyData polyData, long i) { + vtkIdList idList = new vtkIdList(); + polyData.GetCellPoints(i, idList); + + double[] pt = new double[3]; + polyData.GetPoint(idList.GetId(0), pt); + VectorIJK v1 = new VectorIJK(pt); + polyData.GetPoint(idList.GetId(1), pt); + VectorIJK v2 = new VectorIJK(pt); + polyData.GetPoint(idList.GetId(2), pt); + VectorIJK v3 = new VectorIJK(pt); + + return new TriangularFacet(v1, v2, v3); + } + + /** + * Return the vtkIDList for points found within radius of point pt. + * + * @param pt + * @param polyData + * @param pointLocator + * @param radius + * @return + */ + public static vtkIdList getIDsAtPointWithinRadius( + double[] pt, vtkPolyData polyData, vtkAbstractPointLocator pointLocator, double radius) { + + vtkIdList idList = new vtkIdList(); + pointLocator.FindPointsWithinRadius(radius, pt, idList); + return idList; + } + + /** + * A 3D Delaunay triangulation is constructed from the points in region0. Any points from region1 + * contained within this mesh are considered to be in the overlapping region. + * + * @param region0 Reference point set + * @param region1 Test point set + * @return points in region1 contained within region0 + */ + public static vtkUnstructuredGrid intersectingPoints(vtkPoints region0, vtkPoints region1) { + vtkUnstructuredGrid region0Grid = new vtkUnstructuredGrid(); + region0Grid.SetPoints(region0); + + vtkDelaunay3D vtkd = new vtkDelaunay3D(); + vtkd.SetInputData(region0Grid); + vtkd.Update(); + + vtkGeometryFilter vtkgf = new vtkGeometryFilter(); + vtkgf.SetInputData(vtkd.GetOutput()); + vtkgf.Update(); + + vtksbCellLocator cellLoc = new vtksbCellLocator(); + cellLoc.SetDataSet(vtkgf.GetOutput()); + cellLoc.BuildLocator(); + + // vtkPolyDataWriter pdw = new vtkPolyDataWriter(); + // pdw.SetInputData(vtkgf.GetOutput()); + // pdw.SetFileName("./fixedHull.vtk"); + // pdw.SetFileTypeToBinary(); + // pdw.Update(); + + double[] origin = {0., 0., 0.}; + double tol = 1e-6; + double[] t = new double[1]; + double[] x = new double[3]; + double[] pcoords = new double[3]; + int[] subId = new int[1]; + long[] cellId = new long[1]; + vtkGenericCell genericCell = new vtkGenericCell(); + vtkPoints intersected = new vtkPoints(); + for (int i = 0; i < region1.GetNumberOfPoints(); i++) { + double[] p = region1.GetPoint(i); + double[] endPt = new double[3]; + for (int j = 0; j < 3; j++) endPt[j] = p[j] * 10; + int code = cellLoc.IntersectWithLine(origin, endPt, tol, t, x, pcoords, subId, cellId, genericCell); + if (code == 0) continue; + intersected.InsertNextPoint(p); + } + + // System.out.printf("intersectingPoints: %d intersecting points\n", + // intersected.GetNumberOfPoints()); + + vtkUnstructuredGrid returnGrid = new vtkUnstructuredGrid(); + returnGrid.SetPoints(intersected); + + return returnGrid; + } + + /** + * Read in a shape model in ICQ format and convert to PLT format. + * + * @param icqFile + * @return + */ + private static List icq2plt(String icqFile) { + try { + List lines = FileUtils.readLines(new File(icqFile), Charset.defaultCharset()); + return icq2plt(lines); + } catch (IOException e) { + logger.error(e.getLocalizedMessage()); + return null; + } + } + + /** + * Read in a shape model in ICQ format and convert to PLT format. Based on SHAPE2PLATES from SPC. + * + * @param icqLines + * @return + */ + private static List icq2plt(List icqLines) { + + List pltLines = new ArrayList<>(); + + String[] parts = icqLines.get(0).strip().split("\\s+"); + int q = Integer.parseInt(parts[0].strip()); + + pltLines.add(String.format("%d", (int) (6 * Math.pow(q + 1, 2)))); + + double[][][][] vec = new double[3][q + 1][q + 1][6]; + int[][][] n = new int[q + 1][q + 1][6]; + + int n0 = 0; + for (int f = 0; f < 6; f++) { + for (int j = 0; j <= q; j++) { + for (int i = 0; i <= q; i++) { + parts = icqLines.get(++n0).strip().split("\\s+"); + for (int k = 0; k < 3; k++) + vec[k][i][j][f] = Double.parseDouble(parts[k].strip().replaceAll("D", "E")); + pltLines.add(String.format( + "%10d %.12f %.12f %.12f", n0, vec[0][i][j][f], vec[1][i][j][f], vec[2][i][j][f])); + n[i][j][f] = n0; + } + } + } + + for (int i = 1; i < q; i++) { + n[i][q][5] = n[q - i][q][3]; + n[i][0][5] = n[i][q][1]; + n[i][0][4] = n[q][q - i][0]; + n[i][0][3] = n[q - i][0][0]; + n[i][0][2] = n[0][i][0]; + n[i][0][1] = n[i][q][0]; + } + + for (int j = 1; j < q; j++) { + n[q][j][5] = n[j][q][4]; + n[q][j][4] = n[0][j][3]; + n[q][j][3] = n[0][j][2]; + n[q][j][2] = n[0][j][1]; + n[0][j][5] = n[q - j][q][2]; + n[0][j][4] = n[q][j][1]; + } + + n[0][0][2] = n[0][0][0]; + n[q][0][3] = n[0][0][0]; + n[0][0][1] = n[0][q][0]; + n[q][0][2] = n[0][q][0]; + n[0][0][3] = n[q][0][0]; + n[q][0][4] = n[q][0][0]; + n[0][0][4] = n[q][q][0]; + n[q][0][1] = n[q][q][0]; + n[0][0][5] = n[0][q][1]; + n[q][q][2] = n[0][q][1]; + n[0][q][4] = n[q][q][1]; + n[q][0][5] = n[q][q][1]; + n[q][q][3] = n[0][q][2]; + n[0][q][5] = n[0][q][2]; + 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()); + // } + + pltLines.add(String.format("%d", 12 * q * q)); + n0 = 0; + for (int f = 0; f < 6; f++) { + for (int i = 0; i < q; i++) { + for (int j = 0; j < q; j++) { + + // @formatter:off + double w1x = vec[1][i][j][f] * vec[2][i + 1][j + 1][f] - vec[2][i][j][f] * vec[1][i + 1][j + 1][f]; + double w1y = vec[2][i][j][f] * vec[0][i + 1][j + 1][f] - vec[0][i][j][f] * vec[2][i + 1][j + 1][f]; + double w1z = vec[0][i][j][f] * vec[1][i + 1][j + 1][f] - vec[1][i][j][f] * vec[0][i + 1][j + 1][f]; + double w2x = vec[1][i + 1][j][f] * vec[2][i][j + 1][f] - vec[2][i + 1][j][f] * vec[1][i][j + 1][f]; + double w2y = vec[2][i + 1][j][f] * vec[0][i][j + 1][f] - vec[0][i + 1][j][f] * vec[2][i][j + 1][f]; + double w2z = vec[0][i + 1][j][f] * vec[1][i][j + 1][f] - vec[1][i + 1][j][f] * vec[0][i][j + 1][f]; + // @formatter:on + + double z1 = w1x * w1x + w1y * w1y + w1z * w1z; + double z2 = w2x * w2x + w2y * w2y + w2z * w2z; + + if (z1 <= z2) { + + n0++; + pltLines.add(String.format( + "%10d %10d %10d %10d", n0, n[i][j][f], n[i + 1][j + 1][f], n[i + 1][j][f])); + + n0++; + pltLines.add(String.format( + "%10d %10d %10d %10d", n0, n[i][j][f], n[i][j + 1][f], n[i + 1][j + 1][f])); + } else { + n0++; + pltLines.add( + String.format("%10d %10d %10d %10d", n0, n[i][j][f], n[i][j + 1][f], n[i + 1][j][f])); + + n0++; + pltLines.add(String.format( + "%10d %10d %10d %10d", n0, n[i + 1][j][f], n[i][j + 1][f], n[i + 1][j + 1][f])); + } + } + } + } + + return pltLines; + } + + /** + * Read in PDS vertex file format. There are 2 variants of this file. In one the first line + * contains the number of points and the number of cells and then follows the points and vertices. + * In the other variant the first line only contains the number of points, then follows the + * points, then follows a line listing the number of cells followed by the cells. Support both + * variants here. + * + * @param lines + * @return + */ + private static vtkPolyData loadPDSShapeModel(List lines) { + vtkPolyData polydata = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray cells = new vtkCellArray(); + polydata.SetPoints(points); + polydata.SetPolys(cells); + + // Read in the first line which list the number of points and plates + int count = 0; + String val = lines.get(count++).strip(); + String[] vals = val.split("\\s+"); + int numPoints = -1; + int numCells = -1; + if (vals.length == 1) { + numPoints = Integer.parseInt(vals[0]); + } else if (vals.length == 2) { + numPoints = Integer.parseInt(vals[0]); + numCells = Integer.parseInt(vals[1]); + } else { + logger.error("Invalid format"); + return polydata; + } + + for (int j = 0; j < numPoints; ++j) { + vals = lines.get(count++).strip().split("\\s+"); + double x = Double.parseDouble(vals[1]); + double y = Double.parseDouble(vals[2]); + double z = Double.parseDouble(vals[3]); + points.InsertNextPoint(x, y, z); + } + + if (numCells == -1) { + val = lines.get(count++).strip(); + numCells = Integer.parseInt(val); + } + + vtkIdList idList = new vtkIdList(); + idList.SetNumberOfIds(3); + for (int j = 0; j < numCells; ++j) { + vals = lines.get(count++).strip().split("\\s+"); + int idx1 = Integer.parseInt(vals[1]) - 1; + int idx2 = Integer.parseInt(vals[2]) - 1; + int idx3 = Integer.parseInt(vals[3]) - 1; + idList.SetId(0, idx1); + idList.SetId(1, idx2); + idList.SetId(2, idx3); + cells.InsertNextCell(idList); + } + + idList.Delete(); + + return polydata; + } + + /** + * Read in PDS vertex file format. There are 2 variants of this file. In one the first line + * contains the number of points and the number of cells and then follows the points and vertices. + * In the other variant the first line only contains the number of points, then follows the + * points, then follows a line listing the number of cells followed by the cells. Support both + * variants here. + * + * @param filename + * @return + * @throws IOException + */ + private static vtkPolyData loadPDSShapeModel(String filename) { + try { + List lines = FileUtils.readLines(new File(filename), Charset.defaultCharset()); + return loadPDSShapeModel(lines); + } catch (IOException e) { + logger.error(e.getLocalizedMessage()); + return null; + } + } + + /** + * Several PDS shape models are in special format similar to standard Gaskell vertex shape models + * but are zero based and don't have a first column listing the id. + * + * @param filename + * @param inMeters If true, vertices are assumed to be in meters. If false, assumed to be + * kilometers. + * @return + * @throws IOException + */ + private static vtkPolyData loadTempel1AndWild2ShapeModel(String filename, boolean inMeters) throws Exception { + vtkPolyData polydata = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray cells = new vtkCellArray(); + polydata.SetPoints(points); + polydata.SetPolys(cells); + + InputStream fs = new FileInputStream(filename); + InputStreamReader isr = new InputStreamReader(fs); + BufferedReader in = new BufferedReader(isr); + + // Read in the first line which lists the number of points and plates + String val = in.readLine().trim(); + String[] vals = val.split("\\s+"); + int numPoints = -1; + int numCells = -1; + if (vals.length == 2) { + numPoints = Integer.parseInt(vals[0]); + numCells = Integer.parseInt(vals[1]); + } else { + in.close(); + throw new IOException("Format not valid"); + } + + for (int j = 0; j < numPoints; ++j) { + vals = in.readLine().trim().split("\\s+"); + double x = Double.parseDouble(vals[0]); + double y = Double.parseDouble(vals[1]); + double z = Double.parseDouble(vals[2]); + + if (inMeters) { + x /= 1000.0; + y /= 1000.0; + z /= 1000.0; + } + + points.InsertNextPoint(x, y, z); + } + + vtkIdList idList = new vtkIdList(); + idList.SetNumberOfIds(3); + for (int j = 0; j < numCells; ++j) { + vals = in.readLine().trim().split("\\s+"); + int idx1 = Integer.parseInt(vals[0]); + int idx2 = Integer.parseInt(vals[1]); + int idx3 = Integer.parseInt(vals[2]); + idList.SetId(0, idx1); + idList.SetId(1, idx2); + idList.SetId(2, idx3); + cells.InsertNextCell(idList); + } + + idList.Delete(); + + in.close(); + + return polydata; + } + + public static vtkPolyData loadLocalFitsLLRModelN(double[][][] data) throws Exception { + return loadLocalFitsLLRModelN(data, null, null, 0); + } + + /** + * Read in a FITS local shape model (i.e. a surface patch such as a maplet) with format where the + * first 6 planes are lat, lon, radius, x, y, z and return the result as a vtkPolyData. Note only + * the x, y, z planes are read to get the point data. The lat, lon, radius planes are ignored. + * + *

The user must also define the number of planes to skip before parsing the ancillary data and + * storing it in List<vtkFloatArray>. + * + *

For example, if planesToSkip = 6 then any planes after the 6th plane will be stored and + * returned in ancillaryData. The user may decide to set planesToSkip = 0 in which case ALL the + * planes in the fits file will be returned in ancillaryData, including the first 6 planes (lat, + * lon, radius, x, y, z). + * + * @param data + * @param ancillaryData - list of vtkFloatArray found by parsing planes in the fits file. NOTE: + * Any prior content in ancillaryData will be cleared by the method and repopulated with + * results from the fits file! + * @param planeNames + * @param planesToSkip - integer number of the planes to skip before putting planes in + * ancillaryData + * @return + * @throws Exception + */ + public static vtkPolyData loadLocalFitsLLRModelN( + double[][][] data, List ancillaryData, List planeNames, int planesToSkip) + throws Exception { + int xIndex = 3; + int yIndex = 4; + int zIndex = 5; + + vtkIdList idList = new vtkIdList(); + vtkPolyData dem = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + dem.SetPoints(points); + dem.SetPolys(polys); + + int numExtraPlanes = data.length - planesToSkip; + if (ancillaryData != null) { + ancillaryData.clear(); + for (int i = 0; i < numExtraPlanes; ++i) { + vtkFloatArray array = new vtkFloatArray(); + array.SetName(planeNames.get(i + planesToSkip)); + ancillaryData.add(array); + } + } + + int liveSizeX = data[0].length; + int liveSizeY = data[0][0].length; + + int[][] indices = new int[liveSizeX][liveSizeY]; + int c = 0; + int i0, i1, i2, i3; + + // First add points to the vtkPoints array + for (int m = 0; m < liveSizeX; ++m) + for (int n = 0; n < liveSizeY; ++n) { + indices[m][n] = -1; + + double x = data[xIndex][m][n]; + double y = data[yIndex][m][n]; + double z = data[zIndex][m][n]; + + boolean valid = (x != INVALID_VALUE && y != INVALID_VALUE && z != INVALID_VALUE); + + if (valid) { + + double[] pt = {x, y, z}; + points.InsertNextPoint(pt); + + if (ancillaryData != null) { + for (int k = 0; k < numExtraPlanes; ++k) + ancillaryData.get(k).InsertNextTuple1(data[planesToSkip + k][m][n]); + } + + indices[m][n] = c; + + ++c; + } + } + + idList.SetNumberOfIds(3); + + // Now add connectivity information + for (int m = 1; m < liveSizeX; ++m) + for (int n = 1; n < liveSizeY; ++n) { + // Get the indices of the 4 corners of the rectangle to the + // upper left + i0 = indices[m - 1][n - 1]; + i1 = indices[m][n - 1]; + i2 = indices[m - 1][n]; + i3 = indices[m][n]; + + // Add upper left triangle + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } + // Add bottom right triangle + if (i2 >= 0 && i1 >= 0 && i3 >= 0) { + idList.SetId(0, i2); + idList.SetId(1, i1); + idList.SetId(2, i3); + polys.InsertNextCell(idList); + } + } + + vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); + normalsFilter.SetInputData(dem); + normalsFilter.SetComputeCellNormals(0); + normalsFilter.SetComputePointNormals(1); + normalsFilter.SplittingOff(); + normalsFilter.ConsistencyOn(); + normalsFilter.AutoOrientNormalsOff(); + if (needToFlipMapletNormalVectors(dem)) normalsFilter.FlipNormalsOn(); + else normalsFilter.FlipNormalsOff(); + normalsFilter.Update(); + + vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); + dem.DeepCopy(normalsFilterOutput); + + return dem; + } + + /** + * Read in a shape model with format where each line in file consists of lat, lon, and radius, or + * lon, lat, and radius. Note that most of the shape models of Thomas and Stooke in this format + * use west longtude. The only exception is Thomas's Ida model which uses east longitude. + * + * @param filename + * @param westLongitude if true, assume longitude is west, if false assume east + * @return + * @throws Exception + */ + private static vtkPolyData loadLLRShapeModel(String filename, boolean westLongitude) throws Exception { + // We need to load the file in 2 passes. In the first pass + // we figure out the latitude/longitude spacing (both assumed same), + // which column is latitude, and which column is longitude. + // + // It is assumed the following: + // If 0 is the first field of the first column, + // then longitude is the first column. + // If -90 is the first field of the first column, + // then latitude is the first column. + // If 90 is the first field of the first column, + // then latitude is the first column. + // + // These assumptions ensure that the shape models of Thomas, Stooke, and Hudson + // are loaded in correctly. However, other shape models in some other lat, lon + // scheme may not be loaded correctly with this function. + // + // In the second pass, we load the file using the values + // determined in the first pass. + + // First pass + double latLonSpacing = 0.0; + int latIndex = 0; + int lonIndex = 1; + + InputStream fs = new FileInputStream(filename); + InputStreamReader isr = new InputStreamReader(fs); + BufferedReader in = new BufferedReader(isr); + + { + // We only need to look at the first 2 lines of the file + // in the first pass to determine everything we need. + String[] vals = in.readLine().trim().split("\\s+"); + double a1 = Double.parseDouble(vals[0]); + double b1 = Double.parseDouble(vals[1]); + vals = in.readLine().trim().split("\\s+"); + double a2 = Double.parseDouble(vals[0]); + double b2 = Double.parseDouble(vals[1]); + + if (a1 == 0.0) { + latIndex = 1; + lonIndex = 0; + } else if (a1 == -90.0 || a1 == 90.0) { + latIndex = 0; + lonIndex = 1; + } else { + System.err.println("loadLLRShapeModel: Incorrect format for input file"); + } + + if (a1 != a2) latLonSpacing = Math.abs(a2 - a1); + else if (b1 != b2) latLonSpacing = Math.abs(b2 - b1); + else System.err.println("loadLLRShapeModel: Incorrect format for input file"); + + in.close(); + } + + // Second pass + fs = new FileInputStream(filename); + isr = new InputStreamReader(fs); + in = new BufferedReader(isr); + + vtkPolyData body = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + body.SetPoints(points); + body.SetPolys(polys); + + int numRows = (int) Math.round(180.0 / latLonSpacing) + 1; + int numCols = (int) Math.round(360.0 / latLonSpacing) + 1; + + int count = 0; + int[][] indices = new int[numRows][numCols]; + String line; + while ((line = in.readLine()) != null) { + String[] vals = line.trim().split("\\s+"); + double lat = Double.parseDouble(vals[latIndex]); + double lon = Double.parseDouble(vals[lonIndex]); + double rad = Double.parseDouble(vals[2]); + + int row = (int) Math.round((lat + 90.0) / latLonSpacing); + int col = (int) Math.round(lon / latLonSpacing); + + // Only include 1 point at each pole and don't include any points + // at longitude 360 since it's the same as longitude 0 + if ((lat == -90.0 && lon > 0.0) || (lat == 90.0 && lon > 0.0) || lon == 360.0) { + indices[row][col] = -1; + } else { + if (westLongitude) lon = -lon; + + indices[row][col] = count++; + UnwritableVectorIJK v = + CoordConverters.convert(new LatitudinalVector(rad, Math.toRadians(lat), Math.toRadians(lon))); + double[] pt = {v.getI(), v.getJ(), v.getK()}; + points.InsertNextPoint(pt); + } + } + + in.close(); + + // Now add connectivity information + int i0, i1, i2, i3; + vtkIdList idList = new vtkIdList(); + idList.SetNumberOfIds(3); + for (int m = 0; m <= numRows - 2; ++m) + for (int n = 0; n <= numCols - 2; ++n) { + // Add triangles touching south pole + if (m == 0) { + i0 = indices[m][0]; // index of south pole point + i1 = indices[m + 1][n]; + if (n == numCols - 2) i2 = indices[m + 1][0]; + else i2 = indices[m + 1][n + 1]; + + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building south pole facets."); + } + + } + // Add triangles touching north pole + else if (m == numRows - 2) { + i0 = indices[m + 1][0]; // index of north pole point + i1 = indices[m][n]; + if (n == numCols - 2) i2 = indices[m][0]; + else i2 = indices[m][n + 1]; + + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building north pole facets."); + } + } + // Add middle triangles that do not touch either pole + else { + // Get the indices of the 4 corners of the rectangle to the upper right + i0 = indices[m][n]; + i1 = indices[m + 1][n]; + if (n == numCols - 2) { + i2 = indices[m][0]; + i3 = indices[m + 1][0]; + } else { + i2 = indices[m][n + 1]; + i3 = indices[m + 1][n + 1]; + } + + // Add upper left triangle + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building facets."); + } + + // Add bottom right triangle + if (i2 >= 0 && i1 >= 0 && i3 >= 0) { + idList.SetId(0, i2); + idList.SetId(1, i1); + idList.SetId(2, i3); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building facets."); + } + } + } + + // vtkPolyDataWriter writer = new vtkPolyDataWriter(); + // writer.SetInput(body); + // writer.SetFileName("/tmp/coneeros.vtk"); + //// writer.SetFileTypeToBinary(); + // writer.Write(); + + return body; + } + + /** + * This function is used to load the Eros model based on NLR data available from + * http://sbn.psi.edu/pds/resource/nearbrowse.html. It is very similar to the previous function + * but with several subtle differences. + * + * @param filename + * @param westLongitude + * @return + * @throws Exception + */ + private static vtkPolyData loadLLR2ShapeModel(String filename, boolean westLongitude) throws Exception { + double latLonSpacing = 1.0; + int latIndex = 1; + int lonIndex = 0; + + InputStream fs = new FileInputStream(filename); + InputStreamReader isr = new InputStreamReader(fs); + BufferedReader in = new BufferedReader(isr); + + vtkPolyData body = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + body.SetPoints(points); + body.SetPolys(polys); + + int numRows = (int) Math.round(180.0 / latLonSpacing) + 2; + int numCols = (int) Math.round(360.0 / latLonSpacing); + + int count = 0; + int[][] indices = new int[numRows][numCols]; + String line; + double[] northPole = {0.0, 0.0, 0.0}; + double[] southPole = {0.0, 0.0, 0.0}; + + indices[0][0] = count++; + points.InsertNextPoint(southPole); // placeholder for south pole + + while ((line = in.readLine()) != null) { + String[] vals = line.trim().split("\\s+"); + double lat = Double.parseDouble(vals[latIndex]); + double lon = Double.parseDouble(vals[lonIndex]); + double rad = Double.parseDouble(vals[2]) / 1000.0; + + int row = (int) Math.round((lat + 89.5) / latLonSpacing) + 1; + int col = (int) Math.round((lon - 0.5) / latLonSpacing); + + if (westLongitude) lon = -lon; + + indices[row][col] = count++; + UnwritableVectorIJK v = + CoordConverters.convert(new LatitudinalVector(rad, Math.toRadians(lat), Math.toRadians(lon))); + double[] pt = {v.getI(), v.getJ(), v.getK()}; + points.InsertNextPoint(pt); + + // We need to compute the pole points (not included in the file) + // by averaging the points at latitudes 89.5 and -89.5 + if (lat == -89.5) { + southPole[0] += pt[0]; + southPole[1] += pt[1]; + southPole[2] += pt[2]; + } else if (lat == 89.5) { + northPole[0] += pt[0]; + northPole[1] += pt[1]; + northPole[2] += pt[2]; + } + } + + in.close(); + + for (int i = 0; i < 3; ++i) { + southPole[i] /= 360.0; + northPole[i] /= 360.0; + } + + points.SetPoint(0, southPole); + + indices[numRows - 1][0] = count++; + points.InsertNextPoint(northPole); // north pole + + // Now add connectivity information + int i0, i1, i2, i3; + vtkIdList idList = new vtkIdList(); + idList.SetNumberOfIds(3); + for (int m = 0; m <= numRows - 2; ++m) + for (int n = 0; n <= numCols - 1; ++n) { + // Add triangles touching south pole + if (m == 0) { + i0 = indices[m][0]; // index of south pole point + i1 = indices[m + 1][n]; + if (n == numCols - 1) i2 = indices[m + 1][0]; + else i2 = indices[m + 1][n + 1]; + + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building south pole facets."); + } + + } + // Add triangles touching north pole + else if (m == numRows - 2) { + i0 = indices[m + 1][0]; // index of north pole point + i1 = indices[m][n]; + if (n == numCols - 1) i2 = indices[m][0]; + else i2 = indices[m][n + 1]; + + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building north pole facets."); + } + } + // Add middle triangles that do not touch either pole + else { + // Get the indices of the 4 corners of the rectangle to the upper right + i0 = indices[m][n]; + i1 = indices[m + 1][n]; + if (n == numCols - 1) { + i2 = indices[m][0]; + i3 = indices[m + 1][0]; + } else { + i2 = indices[m][n + 1]; + i3 = indices[m + 1][n + 1]; + } + + // Add upper left triangle + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i1); + idList.SetId(2, i2); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building facets."); + } + + // Add bottom right triangle + if (i2 >= 0 && i1 >= 0 && i3 >= 0) { + idList.SetId(0, i2); + idList.SetId(1, i1); + idList.SetId(2, i3); + polys.InsertNextCell(idList); + } else { + System.err.println("loadLLRShapeModel: Error building facets."); + } + } + } + + return body; + } + + private static vtkPolyData loadVTKShapeModel(String filename) throws Exception { + vtkPolyDataReader smallBodyReader = new vtkPolyDataReader(); + smallBodyReader.SetFileName(filename); + smallBodyReader.Update(); + + vtkPolyData output = smallBodyReader.GetOutput(); + + vtkPolyData shapeModel = new vtkPolyData(); + shapeModel.ShallowCopy(output); + + smallBodyReader.Delete(); + + return shapeModel; + } + + private static vtkPolyData loadOBJShapeModel(String filename) throws Exception { + vtkOBJReader smallBodyReader = new vtkOBJReader(); + smallBodyReader.SetFileName(filename); + smallBodyReader.Update(); + + vtkPolyData output = smallBodyReader.GetOutput(); + + vtkPolyData shapeModel = new vtkPolyData(); + shapeModel.ShallowCopy(output); + + smallBodyReader.Delete(); + + return shapeModel; + } + + private static vtkPolyData loadPLYShapeModel(String filename) throws Exception { + vtkPLYReader smallBodyReader = new vtkPLYReader(); + smallBodyReader.SetFileName(filename); + smallBodyReader.Update(); + + vtkPolyData output = smallBodyReader.GetOutput(); + + vtkPolyData shapeModel = new vtkPolyData(); + shapeModel.ShallowCopy(output); + + smallBodyReader.Delete(); + + return shapeModel; + } + + private static vtkPolyData loadSTLShapeModel(String filename) throws Exception { + vtkSTLReader smallBodyReader = new vtkSTLReader(); + smallBodyReader.SetFileName(filename); + smallBodyReader.Update(); + + vtkPolyData output = smallBodyReader.GetOutput(); + + vtkPolyData shapeModel = new vtkPolyData(); + shapeModel.ShallowCopy(output); + + smallBodyReader.Delete(); + + return shapeModel; + } + + public static vtkPolyData loadFITShapeModel(String filename) throws Exception { + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + vtkPolyData shapeModel = new vtkPolyData(); + vtkIdList idList = new vtkIdList(); + shapeModel.SetPoints(points); + shapeModel.SetPolys(polys); + + Fits f = new Fits(filename); + BasicHDU hdu = f.getHDU(0); + + // First pass, figure out number of planes and grab size and scale information + Header header = hdu.getHeader(); + HeaderCard headerCard; + int xIdx = -1; + int yIdx = -1; + int zIdx = -1; + int planeCount = 0; + while ((headerCard = header.nextCard()) != null) { + String headerKey = headerCard.getKey(); + String headerValue = headerCard.getValue(); + + if (headerKey.startsWith("PLANE")) { + // Determine if we are looking at a coordinate or a backplane + if (headerValue.startsWith("X")) { + // This plane is the X coordinate, save the index + xIdx = planeCount; + } else if (headerValue.startsWith("Y")) { + // This plane is the Y coordinate, save the index + yIdx = planeCount; + } else if (headerValue.startsWith("Z")) { + // This plane is the Z coordinate, save the index + zIdx = planeCount; + } + + // Increment plane count + planeCount++; + } + } + + // Check to see if x,y,z planes were all defined + if (xIdx < 0) { + throw new IOException("FITS file does not contain plane for X coordinate"); + } else if (yIdx < 0) { + throw new IOException("FITS file does not contain plane for Y coordinate"); + } else if (zIdx < 0) { + throw new IOException("FITS file does not contain plane for Z coordinate"); + } + + // Check dimensions of actual data + int[] axes = hdu.getAxes(); + if (axes.length != 3 || axes[1] != axes[2]) { + throw new IOException("FITS file has incorrect dimensions"); + } + + int liveSize = axes[1]; + + float[][][] data = (float[][][]) hdu.getData().getData(); + f.getStream().close(); + + int[][] indices = new int[liveSize][liveSize]; + int c = 0; + float x, y, z; + float INVALID_VALUE = -1.0e38f; + + // First add points to the vtkPoints array + for (int m = 0; m < liveSize; ++m) + for (int n = 0; n < liveSize; ++n) { + indices[m][n] = -1; + + // A pixel value of -1.0e38 means that pixel is invalid and should be skipped + x = data[xIdx][m][n]; + y = data[yIdx][m][n]; + z = data[zIdx][m][n]; + + // Check to see if x,y,z values are all valid + boolean valid = x != INVALID_VALUE && y != INVALID_VALUE && z != INVALID_VALUE; + + // Only add point if everything is valid + if (valid) { + points.InsertNextPoint(x, y, z); + indices[m][n] = c; + ++c; + } + } + + idList.SetNumberOfIds(3); + + // Now add connectivity information + int i0, i1, i2, i3; + for (int m = 1; m < liveSize; ++m) + for (int n = 1; n < liveSize; ++n) { + // Get the indices of the 4 corners of the rectangle to the upper left + i0 = indices[m - 1][n - 1]; + i1 = indices[m][n - 1]; + i2 = indices[m - 1][n]; + i3 = indices[m][n]; + + // Add upper left triangle + if (i0 >= 0 && i1 >= 0 && i2 >= 0) { + idList.SetId(0, i0); + idList.SetId(1, i2); + idList.SetId(2, i1); + polys.InsertNextCell(idList); + } + // Add bottom right triangle + if (i2 >= 0 && i1 >= 0 && i3 >= 0) { + idList.SetId(0, i2); + idList.SetId(1, i3); + idList.SetId(2, i1); + polys.InsertNextCell(idList); + } + } + + vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); + normalsFilter.SetInputData(shapeModel); + normalsFilter.SetComputeCellNormals(0); + normalsFilter.SetComputePointNormals(1); + normalsFilter.SplittingOff(); + normalsFilter.FlipNormalsOn(); + normalsFilter.Update(); + + vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); + shapeModel.DeepCopy(normalsFilterOutput); + + return shapeModel; + } + + /** + * This function loads a shape model in a variety of formats. It looks at the file extension to + * determine its format. It supports these formats: + * + *

    + *
  • VTK (.vtk extension) + *
  • OBJ (.obj extension) + *
  • ICQ (.icq) Bob Gaskell's ICQ format + *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) + *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) + *
  • PLY (.ply extension) + *
  • STL (.stl extension) + *
+ * + *

If you want VTK to compute the normals (this may cause the vertex indices per facet to be + * reordered), use {@link #loadShapeModelAndComputeNormals(String)} instead. + * + * @param filename + * @return + * @throws Exception + */ + public static vtkPolyData loadShapeModel(String filename) throws Exception { + if (!new File(filename).exists()) { + logger.warn("loadShapeModel: cannot load " + filename); + return null; + } + + String ext = FilenameUtils.getExtension(filename).toLowerCase(); + return loadShapeModel(filename, ext); + } + + /** + * This function loads a shape model in a variety of formats. + * + *

    + *
  • VTK (.vtk extension) * + *
  • OBJ (.obj extension) * + *
  • ICQ (.icq) Bob Gaskell's ICQ format * + *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) * + *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) * + *
  • PLY (.ply extension) * + *
  • STL (.stl extension) * + *
+ * + * @param filename + * @param ext + * @return + * @throws Exception + */ + public static vtkPolyData loadShapeModel(String filename, String ext) throws Exception { + if (!new File(filename).exists()) { + logger.warn("loadShapeModel: cannot load " + filename); + return null; + } + + ext = ext.toLowerCase(); + + vtkPolyData shapeModel = new vtkPolyData(); + if (ext.equals("vtk")) { + shapeModel = loadVTKShapeModel(filename); + } else if (ext.equals("icq")) { + shapeModel = loadPDSShapeModel(icq2plt(filename)); + } else if (ext.equals("obj") || ext.equals("wf")) { + shapeModel = loadOBJShapeModel(filename); + } else if (ext.equals("pds") || ext.equals("plt") || ext.equals("tab")) { + shapeModel = loadPDSShapeModel(filename); + } else if (ext.equals("llr")) { + boolean westLongitude = true; + // Thomas's Ida shape model uses east longitude. All the others use west longitude. + // TODO rather than hard coding this check in, need better way to decide if model + // uses west or east longitude. + if (filename.toLowerCase().contains("thomas") + && filename.toLowerCase().contains("243ida")) westLongitude = false; + shapeModel = loadLLRShapeModel(filename, westLongitude); + } else if (ext.equals("llr2")) { + shapeModel = loadLLR2ShapeModel(filename, false); + } else if (ext.equals("t1")) { + shapeModel = loadTempel1AndWild2ShapeModel(filename, false); + } else if (ext.equals("w2")) { + shapeModel = loadTempel1AndWild2ShapeModel(filename, true); + } else if (ext.equals("ply")) { + shapeModel = loadPLYShapeModel(filename); + } else if (ext.equals("stl")) { + shapeModel = loadSTLShapeModel(filename); + } else { + logger.warn("Unknown extension: " + FilenameUtils.getExtension(filename)); + return null; + } + return shapeModel; + } + + /** + * This function loads a shape model in a variety of formats. It looks at the file extension to + * determine its format. It supports these formats: + * + *
    + *
  • VTK (.vtk extension) + *
  • OBJ (.obj extension) + *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) + *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) + *
  • PLY (.ply extension) + *
  • STL (.stl extension) + *
+ * + * This function also adds normal vectors to the returned polydata, if not available in the file. + * + *

ONLY TO BE USED WITH CLOSED SHAPE MODELS! Otherwise, "all bets are off" according to the VTK + * documentation. VTK will reorder the vertex indices for a facet if it thinks the normal is + * pointing in the "wrong" direction, which may not be what you want to happen. For a local shape + * model, or if you don't care about the VTK computed normals (this is probably most of the time), + * use {@link #loadShapeModel(String)}. + * + * @param filename + * @return + * @throws Exception + */ + public static vtkPolyData loadShapeModelAndComputeNormals(String filename) throws Exception { + vtkPolyData shapeModel = loadShapeModel(filename); + addPointNormalsToShapeModel(shapeModel); + return shapeModel; + } + + /** + * True if the mean normal and centroid vectors point in opposite directions. Only meaningful for + * local shape models. + * + * @param polydata + * @return + */ + public static boolean needToFlipMapletNormalVectors(vtkPolyData polydata) { + Vector3D normal = computeMeanPolyDataNormal(polydata); + Vector3D centroid = computePolyDataCentroid(polydata); + return normal.dotProduct(centroid) < 0; + } + + /** + * This method removes any duplicate vertices and renumbers the facet index map. The order of + * vertices is preserved. + * + * @param polyDataIn + * @throws Exception + */ + public static vtkPolyData removeDuplicatePoints(vtkPolyData polyDataIn) throws Exception { + + Map> cellPointMap = new HashMap<>(); + vtkIdList idList = new vtkIdList(); + double[] pt = new double[3]; + for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { + polyDataIn.GetCellPoints(i, idList); + List cellPoints = new ArrayList<>(); + for (int j = 0; j < 3; j++) { + polyDataIn.GetPoint(idList.GetId(j), pt); + cellPoints.add(new UnwritableVectorIJK(pt)); + } + cellPointMap.put(i, cellPoints); + } + + // lookup table from vertex to index + LinkedHashMap pointMap = new LinkedHashMap<>(); + for (Integer key : cellPointMap.keySet()) { + List cellPoints = cellPointMap.get(key); + for (UnwritableVectorIJK point : cellPoints) { + if (!pointMap.containsKey(point)) { + pointMap.put(point, pointMap.size()); + } + } + } + + vtkPolyData body = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + body.SetPoints(points); + body.SetPolys(polys); + + List pointList = new ArrayList<>(pointMap.keySet()); + for (int i = 0; i < pointList.size(); i++) { + UnwritableVectorIJK point = pointList.get(i); + points.InsertNextPoint(point.getI(), point.getJ(), point.getK()); + } + + for (Integer i : cellPointMap.keySet()) { + List cellPoints = cellPointMap.get(i); + idList = new vtkIdList(); + for (UnwritableVectorIJK cellPoint : cellPoints) idList.InsertNextId(pointMap.get(cellPoint)); polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building south pole facets."); - } - } - // Add triangles touching north pole - else if (m == numRows - 2) { - i0 = indices[m + 1][0]; // index of north pole point - i1 = indices[m][n]; - if (n == numCols - 2) i2 = indices[m][0]; - else i2 = indices[m][n + 1]; - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); + return body; + } + + /** + * Take the input polydata and create a new polydata with only vertices that are part of facets. + * + * @param polyDataIn + * @return + */ + public static vtkPolyData removeUnreferencedPoints(vtkPolyData polyDataIn) { + + vtkIdList idList = new vtkIdList(); + + // build the set of vertices referenced by any facet + NavigableSet vertexIndices = new TreeSet<>(); + for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { + polyDataIn.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + + vertexIndices.add(id0); + vertexIndices.add(id1); + vertexIndices.add(id2); + } + + vtkPolyData body = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + body.SetPoints(points); + body.SetPolys(polys); + + // create list of points that are referenced by at least one facet, and + // a map from new to old vertex index + NavigableMap indexMap = new TreeMap<>(); + double[] pt = new double[3]; + for (Long index : vertexIndices) { + polyDataIn.GetPoint(index, pt); + points.InsertNextPoint(pt); + indexMap.put(index, indexMap.size()); + } + + for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { + polyDataIn.GetCellPoints(i, idList); + idList.SetId(0, indexMap.get(idList.GetId(0))); + idList.SetId(1, indexMap.get(idList.GetId(1))); + idList.SetId(2, indexMap.get(idList.GetId(2))); polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building north pole facets."); - } - } - // Add middle triangles that do not touch either pole - else { - // Get the indices of the 4 corners of the rectangle to the upper right - i0 = indices[m][n]; - i1 = indices[m + 1][n]; - if (n == numCols - 2) { - i2 = indices[m][0]; - i3 = indices[m + 1][0]; - } else { - i2 = indices[m][n + 1]; - i3 = indices[m + 1][n + 1]; - } - - // Add upper left triangle - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building facets."); - } - - // Add bottom right triangle - if (i2 >= 0 && i1 >= 0 && i3 >= 0) { - idList.SetId(0, i2); - idList.SetId(1, i1); - idList.SetId(2, i3); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building facets."); - } - } - } - - // vtkPolyDataWriter writer = new vtkPolyDataWriter(); - // writer.SetInput(body); - // writer.SetFileName("/tmp/coneeros.vtk"); - //// writer.SetFileTypeToBinary(); - // writer.Write(); - - return body; - } - - /** - * This function is used to load the Eros model based on NLR data available from - * http://sbn.psi.edu/pds/resource/nearbrowse.html. It is very similar to the previous function - * but with several subtle differences. - * - * @param filename - * @param westLongitude - * @return - * @throws Exception - */ - private static vtkPolyData loadLLR2ShapeModel(String filename, boolean westLongitude) - throws Exception { - double latLonSpacing = 1.0; - int latIndex = 1; - int lonIndex = 0; - - InputStream fs = new FileInputStream(filename); - InputStreamReader isr = new InputStreamReader(fs); - BufferedReader in = new BufferedReader(isr); - - vtkPolyData body = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - body.SetPoints(points); - body.SetPolys(polys); - - int numRows = (int) Math.round(180.0 / latLonSpacing) + 2; - int numCols = (int) Math.round(360.0 / latLonSpacing); - - int count = 0; - int[][] indices = new int[numRows][numCols]; - String line; - double[] northPole = {0.0, 0.0, 0.0}; - double[] southPole = {0.0, 0.0, 0.0}; - - indices[0][0] = count++; - points.InsertNextPoint(southPole); // placeholder for south pole - - while ((line = in.readLine()) != null) { - String[] vals = line.trim().split("\\s+"); - double lat = Double.parseDouble(vals[latIndex]); - double lon = Double.parseDouble(vals[lonIndex]); - double rad = Double.parseDouble(vals[2]) / 1000.0; - - int row = (int) Math.round((lat + 89.5) / latLonSpacing) + 1; - int col = (int) Math.round((lon - 0.5) / latLonSpacing); - - if (westLongitude) lon = -lon; - - indices[row][col] = count++; - UnwritableVectorIJK v = - CoordConverters.convert( - new LatitudinalVector(rad, Math.toRadians(lat), Math.toRadians(lon))); - double[] pt = {v.getI(), v.getJ(), v.getK()}; - points.InsertNextPoint(pt); - - // We need to compute the pole points (not included in the file) - // by averaging the points at latitudes 89.5 and -89.5 - if (lat == -89.5) { - southPole[0] += pt[0]; - southPole[1] += pt[1]; - southPole[2] += pt[2]; - } else if (lat == 89.5) { - northPole[0] += pt[0]; - northPole[1] += pt[1]; - northPole[2] += pt[2]; - } - } - - in.close(); - - for (int i = 0; i < 3; ++i) { - southPole[i] /= 360.0; - northPole[i] /= 360.0; - } - - points.SetPoint(0, southPole); - - indices[numRows - 1][0] = count++; - points.InsertNextPoint(northPole); // north pole - - // Now add connectivity information - int i0, i1, i2, i3; - vtkIdList idList = new vtkIdList(); - idList.SetNumberOfIds(3); - for (int m = 0; m <= numRows - 2; ++m) - for (int n = 0; n <= numCols - 1; ++n) { - // Add triangles touching south pole - if (m == 0) { - i0 = indices[m][0]; // index of south pole point - i1 = indices[m + 1][n]; - if (n == numCols - 1) i2 = indices[m + 1][0]; - else i2 = indices[m + 1][n + 1]; - - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building south pole facets."); - } - - } - // Add triangles touching north pole - else if (m == numRows - 2) { - i0 = indices[m + 1][0]; // index of north pole point - i1 = indices[m][n]; - if (n == numCols - 1) i2 = indices[m][0]; - else i2 = indices[m][n + 1]; - - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building north pole facets."); - } - } - // Add middle triangles that do not touch either pole - else { - // Get the indices of the 4 corners of the rectangle to the upper right - i0 = indices[m][n]; - i1 = indices[m + 1][n]; - if (n == numCols - 1) { - i2 = indices[m][0]; - i3 = indices[m + 1][0]; - } else { - i2 = indices[m][n + 1]; - i3 = indices[m + 1][n + 1]; - } - - // Add upper left triangle - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i1); - idList.SetId(2, i2); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building facets."); - } - - // Add bottom right triangle - if (i2 >= 0 && i1 >= 0 && i3 >= 0) { - idList.SetId(0, i2); - idList.SetId(1, i1); - idList.SetId(2, i3); - polys.InsertNextCell(idList); - } else { - System.err.println("loadLLRShapeModel: Error building facets."); - } - } - } - - return body; - } - - private static vtkPolyData loadVTKShapeModel(String filename) throws Exception { - vtkPolyDataReader smallBodyReader = new vtkPolyDataReader(); - smallBodyReader.SetFileName(filename); - smallBodyReader.Update(); - - vtkPolyData output = smallBodyReader.GetOutput(); - - vtkPolyData shapeModel = new vtkPolyData(); - shapeModel.ShallowCopy(output); - - smallBodyReader.Delete(); - - return shapeModel; - } - - private static vtkPolyData loadOBJShapeModel(String filename) throws Exception { - vtkOBJReader smallBodyReader = new vtkOBJReader(); - smallBodyReader.SetFileName(filename); - smallBodyReader.Update(); - - vtkPolyData output = smallBodyReader.GetOutput(); - - vtkPolyData shapeModel = new vtkPolyData(); - shapeModel.ShallowCopy(output); - - smallBodyReader.Delete(); - - return shapeModel; - } - - private static vtkPolyData loadPLYShapeModel(String filename) throws Exception { - vtkPLYReader smallBodyReader = new vtkPLYReader(); - smallBodyReader.SetFileName(filename); - smallBodyReader.Update(); - - vtkPolyData output = smallBodyReader.GetOutput(); - - vtkPolyData shapeModel = new vtkPolyData(); - shapeModel.ShallowCopy(output); - - smallBodyReader.Delete(); - - return shapeModel; - } - - private static vtkPolyData loadSTLShapeModel(String filename) throws Exception { - vtkSTLReader smallBodyReader = new vtkSTLReader(); - smallBodyReader.SetFileName(filename); - smallBodyReader.Update(); - - vtkPolyData output = smallBodyReader.GetOutput(); - - vtkPolyData shapeModel = new vtkPolyData(); - shapeModel.ShallowCopy(output); - - smallBodyReader.Delete(); - - return shapeModel; - } - - public static vtkPolyData loadFITShapeModel(String filename) throws Exception { - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - vtkPolyData shapeModel = new vtkPolyData(); - vtkIdList idList = new vtkIdList(); - shapeModel.SetPoints(points); - shapeModel.SetPolys(polys); - - Fits f = new Fits(filename); - BasicHDU hdu = f.getHDU(0); - - // First pass, figure out number of planes and grab size and scale information - Header header = hdu.getHeader(); - HeaderCard headerCard; - int xIdx = -1; - int yIdx = -1; - int zIdx = -1; - int planeCount = 0; - while ((headerCard = header.nextCard()) != null) { - String headerKey = headerCard.getKey(); - String headerValue = headerCard.getValue(); - - if (headerKey.startsWith("PLANE")) { - // Determine if we are looking at a coordinate or a backplane - if (headerValue.startsWith("X")) { - // This plane is the X coordinate, save the index - xIdx = planeCount; - } else if (headerValue.startsWith("Y")) { - // This plane is the Y coordinate, save the index - yIdx = planeCount; - } else if (headerValue.startsWith("Z")) { - // This plane is the Z coordinate, save the index - zIdx = planeCount; } - // Increment plane count - planeCount++; - } + return body; } - // Check to see if x,y,z planes were all defined - if (xIdx < 0) { - throw new IOException("FITS file does not contain plane for X coordinate"); - } else if (yIdx < 0) { - throw new IOException("FITS file does not contain plane for Y coordinate"); - } else if (zIdx < 0) { - throw new IOException("FITS file does not contain plane for Z coordinate"); - } + public static vtkPolyData removeZeroAreaFacets(vtkPolyData polyDataIn) { + vtkPolyData body = new vtkPolyData(); + vtkPoints points = new vtkPoints(); + vtkCellArray polys = new vtkCellArray(); + body.SetPoints(points); + body.SetPolys(polys); - // Check dimensions of actual data - int[] axes = hdu.getAxes(); - if (axes.length != 3 || axes[1] != axes[2]) { - throw new IOException("FITS file has incorrect dimensions"); - } - - int liveSize = axes[1]; - - float[][][] data = (float[][][]) hdu.getData().getData(); - f.getStream().close(); - - int[][] indices = new int[liveSize][liveSize]; - int c = 0; - float x, y, z; - float INVALID_VALUE = -1.0e38f; - - // First add points to the vtkPoints array - for (int m = 0; m < liveSize; ++m) - for (int n = 0; n < liveSize; ++n) { - indices[m][n] = -1; - - // A pixel value of -1.0e38 means that pixel is invalid and should be skipped - x = data[xIdx][m][n]; - y = data[yIdx][m][n]; - z = data[zIdx][m][n]; - - // Check to see if x,y,z values are all valid - boolean valid = x != INVALID_VALUE && y != INVALID_VALUE && z != INVALID_VALUE; - - // Only add point if everything is valid - if (valid) { - points.InsertNextPoint(x, y, z); - indices[m][n] = c; - ++c; + double[] pt = new double[3]; + for (int i = 0; i < polyDataIn.GetNumberOfPoints(); i++) { + polyDataIn.GetPoint(i, pt); + points.InsertNextPoint(pt); } - } - idList.SetNumberOfIds(3); + vtkIdList idList = new vtkIdList(); + double[] pt0 = new double[3]; + double[] pt1 = new double[3]; + double[] pt2 = new double[3]; - // Now add connectivity information - int i0, i1, i2, i3; - for (int m = 1; m < liveSize; ++m) - for (int n = 1; n < liveSize; ++n) { - // Get the indices of the 4 corners of the rectangle to the upper left - i0 = indices[m - 1][n - 1]; - i1 = indices[m][n - 1]; - i2 = indices[m - 1][n]; - i3 = indices[m][n]; + for (int i = 0; i < polyDataIn.GetNumberOfCells(); ++i) { + polyDataIn.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + polyDataIn.GetPoint(id0, pt0); + polyDataIn.GetPoint(id1, pt1); + polyDataIn.GetPoint(id2, pt2); - // Add upper left triangle - if (i0 >= 0 && i1 >= 0 && i2 >= 0) { - idList.SetId(0, i0); - idList.SetId(1, i2); - idList.SetId(2, i1); - polys.InsertNextCell(idList); + TriangularFacet facet = new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); + double area = facet.getArea(); + if (area > 0) polys.InsertNextCell(idList); } - // Add bottom right triangle - if (i2 >= 0 && i1 >= 0 && i3 >= 0) { - idList.SetId(0, i2); - idList.SetId(1, i3); - idList.SetId(2, i1); - polys.InsertNextCell(idList); + + return body; + } + + public static void saveShapeModelAsPLT(vtkPolyData polyData, String filename) throws IOException { + // This saves it out in exactly the same format as Bob Gaskell's shape + // models including precision and field width. That's why there's + // extra space padded at the end to make all lines the same length. + + FileWriter fstream = new FileWriter(filename); + BufferedWriter out = new BufferedWriter(fstream); + + vtkPoints points = polyData.GetPoints(); + + long numberPoints = polyData.GetNumberOfPoints(); + long numberCells = polyData.GetNumberOfCells(); + out.write(String.format("%12d %12d \r\n", numberPoints, numberCells)); + + double[] p = new double[3]; + for (int i = 0; i < numberPoints; ++i) { + points.GetPoint(i, p); + out.write(String.format("%10d%15.5f%15.5f%15.5f\r\n", (i + 1), p[0], p[1], p[2])); } - } - vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); - normalsFilter.SetInputData(shapeModel); - normalsFilter.SetComputeCellNormals(0); - normalsFilter.SetComputePointNormals(1); - normalsFilter.SplittingOff(); - normalsFilter.FlipNormalsOn(); - normalsFilter.Update(); - - vtkPolyData normalsFilterOutput = normalsFilter.GetOutput(); - shapeModel.DeepCopy(normalsFilterOutput); - - return shapeModel; - } - - /** - * This function loads a shape model in a variety of formats. It looks at the file extension to - * determine its format. It supports these formats: - * - *

    - *
  • VTK (.vtk extension) - *
  • OBJ (.obj extension) - *
  • ICQ (.icq) Bob Gaskell's ICQ format - *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) - *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) - *
  • PLY (.ply extension) - *
  • STL (.stl extension) - *
- * - *

If you want VTK to compute the normals (this may cause the vertex indices per facet to be - * reordered), use {@link #loadShapeModelAndComputeNormals(String)} instead. - * - * @param filename - * @return - * @throws Exception - */ - public static vtkPolyData loadShapeModel(String filename) throws Exception { - if (!new File(filename).exists()) { - logger.warn("loadShapeModel: cannot load " + filename); - return null; - } - - String ext = FilenameUtils.getExtension(filename).toLowerCase(); - return loadShapeModel(filename, ext); - } - - /** - * This function loads a shape model in a variety of formats. - * - *

    - *
  • VTK (.vtk extension) * - *
  • OBJ (.obj extension) * - *
  • ICQ (.icq) Bob Gaskell's ICQ format * - *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) * - *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) * - *
  • PLY (.ply extension) * - *
  • STL (.stl extension) * - *
- * - * @param filename - * @param ext - * @return - * @throws Exception - */ - public static vtkPolyData loadShapeModel(String filename, String ext) throws Exception { - if (!new File(filename).exists()) { - logger.warn("loadShapeModel: cannot load " + filename); - return null; - } - - ext = ext.toLowerCase(); - - vtkPolyData shapeModel = new vtkPolyData(); - if (ext.equals("vtk")) { - shapeModel = loadVTKShapeModel(filename); - } else if (ext.equals("icq")) { - shapeModel = loadPDSShapeModel(icq2plt(filename)); - } else if (ext.equals("obj") || ext.equals("wf")) { - shapeModel = loadOBJShapeModel(filename); - } else if (ext.equals("pds") || ext.equals("plt") || ext.equals("tab")) { - shapeModel = loadPDSShapeModel(filename); - } else if (ext.equals("llr")) { - boolean westLongitude = true; - // Thomas's Ida shape model uses east longitude. All the others use west longitude. - // TODO rather than hard coding this check in, need better way to decide if model - // uses west or east longitude. - if (filename.toLowerCase().contains("thomas") && filename.toLowerCase().contains("243ida")) - westLongitude = false; - shapeModel = loadLLRShapeModel(filename, westLongitude); - } else if (ext.equals("llr2")) { - shapeModel = loadLLR2ShapeModel(filename, false); - } else if (ext.equals("t1")) { - shapeModel = loadTempel1AndWild2ShapeModel(filename, false); - } else if (ext.equals("w2")) { - shapeModel = loadTempel1AndWild2ShapeModel(filename, true); - } else if (ext.equals("ply")) { - shapeModel = loadPLYShapeModel(filename); - } else if (ext.equals("stl")) { - shapeModel = loadSTLShapeModel(filename); - } else { - logger.warn("Unknown extension: " + FilenameUtils.getExtension(filename)); - return null; - } - return shapeModel; - } - - /** - * This function loads a shape model in a variety of formats. It looks at the file extension to - * determine its format. It supports these formats: - * - *
    - *
  • VTK (.vtk extension) - *
  • OBJ (.obj extension) - *
  • PDS vertex style shape models (.pds, .plt, or .tab extension) - *
  • Lat, lon, radius format also used in PDS shape models (.llr extension) - *
  • PLY (.ply extension) - *
  • STL (.stl extension) - *
- * - * This function also adds normal vectors to the returned polydata, if not available in the file. - * - *

ONLY TO BE USED WITH CLOSED SHAPE MODELS! Otherwise, "all bets are off" according to the VTK - * documentation. VTK will reorder the vertex indices for a facet if it thinks the normal is - * pointing in the "wrong" direction, which may not be what you want to happen. For a local shape - * model, or if you don't care about the VTK computed normals (this is probably most of the time), - * use {@link #loadShapeModel(String)}. - * - * @param filename - * @return - * @throws Exception - */ - public static vtkPolyData loadShapeModelAndComputeNormals(String filename) throws Exception { - vtkPolyData shapeModel = loadShapeModel(filename); - addPointNormalsToShapeModel(shapeModel); - return shapeModel; - } - - /** - * True if the mean normal and centroid vectors point in opposite directions. Only meaningful for - * local shape models. - * - * @param polydata - * @return - */ - public static boolean needToFlipMapletNormalVectors(vtkPolyData polydata) { - Vector3D normal = computeMeanPolyDataNormal(polydata); - Vector3D centroid = computePolyDataCentroid(polydata); - return normal.dotProduct(centroid) < 0; - } - - /** - * This method removes any duplicate vertices and renumbers the facet index map. The order of - * vertices is preserved. - * - * @param polyDataIn - * @throws Exception - */ - public static vtkPolyData removeDuplicatePoints(vtkPolyData polyDataIn) throws Exception { - - Map> cellPointMap = new HashMap<>(); - vtkIdList idList = new vtkIdList(); - double[] pt = new double[3]; - for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { - polyDataIn.GetCellPoints(i, idList); - List cellPoints = new ArrayList<>(); - for (int j = 0; j < 3; j++) { - polyDataIn.GetPoint(idList.GetId(j), pt); - cellPoints.add(new UnwritableVectorIJK(pt)); - } - cellPointMap.put(i, cellPoints); - } - - // lookup table from vertex to index - LinkedHashMap pointMap = new LinkedHashMap<>(); - for (Integer key : cellPointMap.keySet()) { - List cellPoints = cellPointMap.get(key); - for (UnwritableVectorIJK point : cellPoints) { - if (!pointMap.containsKey(point)) { - pointMap.put(point, pointMap.size()); + polyData.BuildCells(); + vtkIdList idList = new vtkIdList(); + for (int i = 0; i < numberCells; ++i) { + polyData.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + out.write(String.format("%10d%10d%10d%10d \r\n", (i + 1), (id0 + 1), (id1 + 1), (id2 + 1))); } - } + + idList.Delete(); + out.close(); } - vtkPolyData body = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - body.SetPoints(points); - body.SetPolys(polys); - - List pointList = new ArrayList<>(pointMap.keySet()); - for (int i = 0; i < pointList.size(); i++) { - UnwritableVectorIJK point = pointList.get(i); - points.InsertNextPoint(point.getI(), point.getJ(), point.getK()); + public static void saveShapeModelAsOBJ(vtkPolyData polyData, String filename) + throws FileNotFoundException, IOException { + saveShapeModelAsOBJ(polyData, filename, null); } - for (Integer i : cellPointMap.keySet()) { - List cellPoints = cellPointMap.get(i); - idList = new vtkIdList(); - for (UnwritableVectorIJK cellPoint : cellPoints) idList.InsertNextId(pointMap.get(cellPoint)); - polys.InsertNextCell(idList); + public static void saveShapeModelAsOBJ(vtkPolyData polyData, String filename, String header) + throws FileNotFoundException, IOException { + try (FileOutputStream fos = new FileOutputStream(new File(filename))) { + saveShapeModelAsOBJ(polyData, fos, header); + } } - return body; - } - - /** - * Take the input polydata and create a new polydata with only vertices that are part of facets. - * - * @param polyDataIn - * @return - */ - public static vtkPolyData removeUnreferencedPoints(vtkPolyData polyDataIn) { - - vtkIdList idList = new vtkIdList(); - - // build the set of vertices referenced by any facet - NavigableSet vertexIndices = new TreeSet<>(); - for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { - polyDataIn.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - - vertexIndices.add(id0); - vertexIndices.add(id1); - vertexIndices.add(id2); + public static void saveShapeModelAsOBJ(vtkPolyData polyData, OutputStream stream) throws IOException { + saveShapeModelAsOBJ(polyData, stream, null); } - vtkPolyData body = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - body.SetPoints(points); - body.SetPolys(polys); + public static void saveShapeModelAsOBJ(vtkPolyData polyData, OutputStream stream, String header) + throws IOException { + OutputStreamWriter fstream = new OutputStreamWriter(stream); + BufferedWriter out = new BufferedWriter(fstream); - // create list of points that are referenced by at least one facet, and - // a map from new to old vertex index - NavigableMap indexMap = new TreeMap<>(); - double[] pt = new double[3]; - for (Long index : vertexIndices) { - polyDataIn.GetPoint(index, pt); - points.InsertNextPoint(pt); - indexMap.put(index, indexMap.size()); + if (header != null) out.write(header); + + vtkPoints points = polyData.GetPoints(); + + long numberPoints = polyData.GetNumberOfPoints(); + long numberCells = polyData.GetNumberOfCells(); + + double[] p = new double[3]; + for (int i = 0; i < numberPoints; ++i) { + points.GetPoint(i, p); + out.write(String.format("v %20.16f %20.16f %20.16f\r\n", p[0], p[1], p[2])); + } + + polyData.BuildCells(); + vtkIdList idList = new vtkIdList(); + for (int i = 0; i < numberCells; ++i) { + polyData.GetCellPoints(i, idList); + long id0 = idList.GetId(0); + long id1 = idList.GetId(1); + long id2 = idList.GetId(2); + out.write(String.format("%-10s%10d%10d%10d\r\n", "f", id0 + 1, id1 + 1, id2 + 1)); + } + + idList.Delete(); + out.close(); } - for (int i = 0; i < polyDataIn.GetNumberOfCells(); i++) { - polyDataIn.GetCellPoints(i, idList); - idList.SetId(0, indexMap.get(idList.GetId(0))); - idList.SetId(1, indexMap.get(idList.GetId(1))); - idList.SetId(2, indexMap.get(idList.GetId(2))); - polys.InsertNextCell(idList); + public static void saveShapeModelAsVTK(vtkPolyData polyData, String filename) throws IOException { + // First make a copy of polydata and remove all cell and point data since we don't want to save + // that out + vtkPolyData newpolydata = new vtkPolyData(); + newpolydata.DeepCopy(polyData); + newpolydata.GetPointData().Reset(); + newpolydata.GetCellData().Reset(); + + // regenerate point normals + vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); + normalsFilter.SetInputData(newpolydata); + normalsFilter.SetComputeCellNormals(0); + normalsFilter.SetComputePointNormals(1); + normalsFilter.AutoOrientNormalsOn(); + normalsFilter.SplittingOff(); + normalsFilter.Update(); + + vtkPolyDataWriter writer = new vtkPolyDataWriter(); + writer.SetInputConnection(normalsFilter.GetOutputPort()); + writer.SetFileName(filename); + writer.SetFileTypeToBinary(); + writer.Write(); } - return body; - } + public static void saveShapeModelAsSTL(vtkPolyData polyData, String filename) throws IOException { + // First make a copy of polydata and remove all cell and point data since we don't want to save + // that out + vtkPolyData newpolydata = new vtkPolyData(); + newpolydata.DeepCopy(polyData); + newpolydata.GetPointData().Reset(); + newpolydata.GetCellData().Reset(); - public static vtkPolyData removeZeroAreaFacets(vtkPolyData polyDataIn) { - vtkPolyData body = new vtkPolyData(); - vtkPoints points = new vtkPoints(); - vtkCellArray polys = new vtkCellArray(); - body.SetPoints(points); - body.SetPolys(polys); - - double[] pt = new double[3]; - for (int i = 0; i < polyDataIn.GetNumberOfPoints(); i++) { - polyDataIn.GetPoint(i, pt); - points.InsertNextPoint(pt); + vtkSTLWriter writer = new vtkSTLWriter(); + writer.SetInputData(newpolydata); + writer.SetFileName(filename); + writer.SetFileTypeToBinary(); + writer.Write(); } - vtkIdList idList = new vtkIdList(); - double[] pt0 = new double[3]; - double[] pt1 = new double[3]; - double[] pt2 = new double[3]; + public static void writeVTKPoints(String outFile, List points) { - for (int i = 0; i < polyDataIn.GetNumberOfCells(); ++i) { - polyDataIn.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - polyDataIn.GetPoint(id0, pt0); - polyDataIn.GetPoint(id1, pt1); - polyDataIn.GetPoint(id2, pt2); + vtkPoints pointsXYZ = new vtkPoints(); + for (Vector3D point : points) { + double[] array = new double[] {point.getX(), point.getY(), point.getZ()}; + pointsXYZ.InsertNextPoint(array); + } - TriangularFacet facet = - new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2)); - double area = facet.getArea(); - if (area > 0) polys.InsertNextCell(idList); + vtkPolyData polyData = new vtkPolyData(); + polyData.SetPoints(pointsXYZ); + + vtkCellArray cells = new vtkCellArray(); + polyData.SetPolys(cells); + + for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { + vtkIdList idList = new vtkIdList(); + idList.InsertNextId(i); + cells.InsertNextCell(idList); + } + + vtkPolyDataWriter writer = new vtkPolyDataWriter(); + writer.SetInputData(polyData); + writer.SetFileName(outFile); + writer.SetFileTypeToBinary(); + writer.Update(); } - - return body; - } - - public static void saveShapeModelAsPLT(vtkPolyData polyData, String filename) throws IOException { - // This saves it out in exactly the same format as Bob Gaskell's shape - // models including precision and field width. That's why there's - // extra space padded at the end to make all lines the same length. - - FileWriter fstream = new FileWriter(filename); - BufferedWriter out = new BufferedWriter(fstream); - - vtkPoints points = polyData.GetPoints(); - - long numberPoints = polyData.GetNumberOfPoints(); - long numberCells = polyData.GetNumberOfCells(); - out.write( - String.format("%12d %12d \r\n", numberPoints, numberCells)); - - double[] p = new double[3]; - for (int i = 0; i < numberPoints; ++i) { - points.GetPoint(i, p); - out.write(String.format("%10d%15.5f%15.5f%15.5f\r\n", (i + 1), p[0], p[1], p[2])); - } - - polyData.BuildCells(); - vtkIdList idList = new vtkIdList(); - for (int i = 0; i < numberCells; ++i) { - polyData.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - out.write( - String.format( - "%10d%10d%10d%10d \r\n", (i + 1), (id0 + 1), (id1 + 1), (id2 + 1))); - } - - idList.Delete(); - out.close(); - } - - public static void saveShapeModelAsOBJ(vtkPolyData polyData, String filename) - throws FileNotFoundException, IOException { - saveShapeModelAsOBJ(polyData, filename, null); - } - - public static void saveShapeModelAsOBJ(vtkPolyData polyData, String filename, String header) - throws FileNotFoundException, IOException { - try (FileOutputStream fos = new FileOutputStream(new File(filename))) { - saveShapeModelAsOBJ(polyData, fos, header); - } - } - - public static void saveShapeModelAsOBJ(vtkPolyData polyData, OutputStream stream) - throws IOException { - saveShapeModelAsOBJ(polyData, stream, null); - } - - public static void saveShapeModelAsOBJ(vtkPolyData polyData, OutputStream stream, String header) - throws IOException { - OutputStreamWriter fstream = new OutputStreamWriter(stream); - BufferedWriter out = new BufferedWriter(fstream); - - if (header != null) out.write(header); - - vtkPoints points = polyData.GetPoints(); - - long numberPoints = polyData.GetNumberOfPoints(); - long numberCells = polyData.GetNumberOfCells(); - - double[] p = new double[3]; - for (int i = 0; i < numberPoints; ++i) { - points.GetPoint(i, p); - out.write(String.format("v %20.16f %20.16f %20.16f\r\n", p[0], p[1], p[2])); - } - - polyData.BuildCells(); - vtkIdList idList = new vtkIdList(); - for (int i = 0; i < numberCells; ++i) { - polyData.GetCellPoints(i, idList); - long id0 = idList.GetId(0); - long id1 = idList.GetId(1); - long id2 = idList.GetId(2); - out.write(String.format("%-10s%10d%10d%10d\r\n", "f", id0 + 1, id1 + 1, id2 + 1)); - } - - idList.Delete(); - out.close(); - } - - public static void saveShapeModelAsVTK(vtkPolyData polyData, String filename) throws IOException { - // First make a copy of polydata and remove all cell and point data since we don't want to save - // that out - vtkPolyData newpolydata = new vtkPolyData(); - newpolydata.DeepCopy(polyData); - newpolydata.GetPointData().Reset(); - newpolydata.GetCellData().Reset(); - - // regenerate point normals - vtkPolyDataNormals normalsFilter = new vtkPolyDataNormals(); - normalsFilter.SetInputData(newpolydata); - normalsFilter.SetComputeCellNormals(0); - normalsFilter.SetComputePointNormals(1); - normalsFilter.AutoOrientNormalsOn(); - normalsFilter.SplittingOff(); - normalsFilter.Update(); - - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputConnection(normalsFilter.GetOutputPort()); - writer.SetFileName(filename); - writer.SetFileTypeToBinary(); - writer.Write(); - } - - public static void saveShapeModelAsSTL(vtkPolyData polyData, String filename) throws IOException { - // First make a copy of polydata and remove all cell and point data since we don't want to save - // that out - vtkPolyData newpolydata = new vtkPolyData(); - newpolydata.DeepCopy(polyData); - newpolydata.GetPointData().Reset(); - newpolydata.GetCellData().Reset(); - - vtkSTLWriter writer = new vtkSTLWriter(); - writer.SetInputData(newpolydata); - writer.SetFileName(filename); - writer.SetFileTypeToBinary(); - writer.Write(); - } - - public static void writeVTKPoints(String outFile, List points) { - - vtkPoints pointsXYZ = new vtkPoints(); - for (Vector3D point : points) { - double[] array = new double[] {point.getX(), point.getY(), point.getZ()}; - pointsXYZ.InsertNextPoint(array); - } - - vtkPolyData polyData = new vtkPolyData(); - polyData.SetPoints(pointsXYZ); - - vtkCellArray cells = new vtkCellArray(); - polyData.SetPolys(cells); - - for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) { - vtkIdList idList = new vtkIdList(); - idList.InsertNextId(i); - cells.InsertNextCell(idList); - } - - vtkPolyDataWriter writer = new vtkPolyDataWriter(); - writer.SetInputData(polyData); - writer.SetFileName(outFile); - writer.SetFileTypeToBinary(); - writer.Update(); - } } diff --git a/src/main/java/terrasaur/utils/ProcessUtils.java b/src/main/java/terrasaur/utils/ProcessUtils.java index fa17ea8..8e14aa1 100644 --- a/src/main/java/terrasaur/utils/ProcessUtils.java +++ b/src/main/java/terrasaur/utils/ProcessUtils.java @@ -31,78 +31,76 @@ import org.apache.logging.log4j.Logger; /** * Methods to run shell commands. - * + * * @author Hari.Nair@jhuapl.edu * */ public class ProcessUtils { - private final static Logger logger = LogManager.getLogger(ProcessUtils.class); + private static final Logger logger = LogManager.getLogger(ProcessUtils.class); - /** - * Calls runProgramAndWait(program, null). - * - * @param program - * @return - * @throws IOException - * @throws InterruptedException - */ - public static boolean runProgramAndWait(String program) throws IOException, InterruptedException { - return runProgramAndWait(program, null); - } + /** + * Calls runProgramAndWait(program, null). + * + * @param program + * @return + * @throws IOException + * @throws InterruptedException + */ + public static boolean runProgramAndWait(String program) throws IOException, InterruptedException { + return runProgramAndWait(program, null); + } - /** - * Calls runProgramAndWait(program, workingDirectory, true). - * - * @param program - * @param workingDirectory - * @return - * @throws IOException - * @throws InterruptedException - */ - public static boolean runProgramAndWait(String program, File workingDirectory) - throws IOException, InterruptedException { - return runProgramAndWait(program, workingDirectory, true); - } + /** + * Calls runProgramAndWait(program, workingDirectory, true). + * + * @param program + * @param workingDirectory + * @return + * @throws IOException + * @throws InterruptedException + */ + public static boolean runProgramAndWait(String program, File workingDirectory) + throws IOException, InterruptedException { + return runProgramAndWait(program, workingDirectory, true); + } - /** - * Run a shell command. - * - * @param program command to run - * @param workingDirectory working directory - * @param printOutput if true, print output to screen - * @return true on successful completion - * @throws IOException - * @throws InterruptedException - */ - public static boolean runProgramAndWait(String program, File workingDirectory, - boolean printOutput) throws IOException, InterruptedException { - ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+")); - processBuilder.directory(workingDirectory); - processBuilder.redirectErrorStream(true); - Process process = processBuilder.start(); + /** + * Run a shell command. + * + * @param program command to run + * @param workingDirectory working directory + * @param printOutput if true, print output to screen + * @return true on successful completion + * @throws IOException + * @throws InterruptedException + */ + public static boolean runProgramAndWait(String program, File workingDirectory, boolean printOutput) + throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+")); + processBuilder.directory(workingDirectory); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); - if (printOutput) { - try ( - BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - logger.info(String.format("Output of running %s is:", program)); - while ((line = br.readLine()) != null) { - logger.info(line); + if (printOutput) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + logger.info(String.format("Output of running %s is:", program)); + while ((line = br.readLine()) != null) { + logger.info(line); + } + } } - } + + int exitStatus = process.waitFor(); + logger.info("Program " + program + " finished with status: " + exitStatus); + process.destroy(); + + if (exitStatus != 0) { + logger.warn("Terminating since subprogram failed."); + System.exit(exitStatus); + } + + return exitStatus == 0; } - - int exitStatus = process.waitFor(); - logger.info("Program " + program + " finished with status: " + exitStatus); - process.destroy(); - - if (exitStatus != 0) { - logger.warn("Terminating since subprogram failed."); - System.exit(exitStatus); - } - - return exitStatus == 0; - } - } diff --git a/src/main/java/terrasaur/utils/RemoveAberration.java b/src/main/java/terrasaur/utils/RemoveAberration.java index f5445f3..53ae6f9 100644 --- a/src/main/java/terrasaur/utils/RemoveAberration.java +++ b/src/main/java/terrasaur/utils/RemoveAberration.java @@ -22,9 +22,9 @@ */ package terrasaur.utils; +import net.jafama.FastMath; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import net.jafama.FastMath; import picante.math.functions.DifferentiableUnivariateFunction; import picante.math.functions.DifferentiableUnivariateFunctions; import picante.math.functions.UnivariateFunction; @@ -53,161 +53,153 @@ import spice.basic.Vector3; *

  • The geometric position and velocity of the target relative to the SSB
  • *
  • The geometric velocity of the observer relative to the SSB
  • * - * + * * This class assumes all necessary kernels have been loaded. The input position vector is assumed * to be a better estimate of the apparent position than the value calculated from the SPICE * kernels. The SPICE kernels are used to get an initial estimate of the aberration. - * + * * @author nairah1 * */ public class RemoveAberration { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - private Body target; - private Body observer; - private final ReferenceFrame J2000; - private final AberrationCorrection NONE; - private final Body SSB; + private Body target; + private Body observer; + private final ReferenceFrame J2000; + private final AberrationCorrection NONE; + private final Body SSB; - public RemoveAberration(Body target, Body observer) throws SpiceException { - this.target = target; - this.observer = observer; - J2000 = new ReferenceFrame("J2000"); - NONE = new AberrationCorrection("NONE"); - SSB = new Body(0); - } - - /** - * - * @param t ephemeris time - * @param targetLTS observer to target vector calculated with LT+S. This must be in J2000. - * @return a geometric vector from observer to target in the inertial frame at time t - * @throws SpiceException - */ - public Vector3 getGeometricPosition(TDBTime t, Vector3 targetLTS) throws SpiceException { - - TDBDuration lightTime = new TDBDuration(targetLTS.norm() / PhysicalConstants.CLIGHT); - TDBTime tmlt = t.sub(lightTime); - - // geometric states of target and observer - StateRecord targetState = new StateRecord(target, tmlt, J2000, NONE, SSB); - StateRecord observerState = new StateRecord(observer, t, J2000, NONE, SSB); - - // estimate aberration magnitude - StateRecord spiceLT = - new StateRecord(target, t, J2000, new AberrationCorrection("LT"), observer); - StateRecord spiceLTS = - new StateRecord(target, t, J2000, new AberrationCorrection("LT+S"), observer); - - double angle = spiceLT.getPosition().sep(spiceLTS.getPosition()); - double step = angle / 10; - - DifferentiableUnivariateFunction func = dfunc(targetLTS, observerState.getVelocity(), step); - - RootFinder finder = RootFinder.create(); - Stepper stepper = Steppers.createConstant(step); - - IntervalSet boundingInterval = - IntervalSet.create(new UnwritableInterval(-2 * angle, 2 * angle)); - - IntervalSet angle0 = finder.locateValue(func, 0., boundingInterval, stepper); - - Vector3 geometricObsToTarget = new Vector3(); - if (angle0.size() > 0) { - - Vector3 axis = targetLTS.cross(observerState.getVelocity()); - Matrix33 m = new AxisAndAngle(axis, angle0.get(0).getBegin()).toMatrix(); - - // this is target - observer - Vector3 targetLT = m.mxv(targetLTS); - - // targetState was evaluated at t - lt. This is the observer position relative to the SSB. - Vector3 observerPos = targetState.getPosition().sub(targetLT); - targetState = new StateRecord(target, t, J2000, NONE, SSB); - geometricObsToTarget = targetState.getPosition().sub(observerPos); - } else { - logger.error(this.getClass().getSimpleName() + ": no solution"); - System.exit(0); + public RemoveAberration(Body target, Body observer) throws SpiceException { + this.target = target; + this.observer = observer; + J2000 = new ReferenceFrame("J2000"); + NONE = new AberrationCorrection("NONE"); + SSB = new Body(0); } - return geometricObsToTarget; - } + /** + * + * @param t ephemeris time + * @param targetLTS observer to target vector calculated with LT+S. This must be in J2000. + * @return a geometric vector from observer to target in the inertial frame at time t + * @throws SpiceException + */ + public Vector3 getGeometricPosition(TDBTime t, Vector3 targetLTS) throws SpiceException { - /** - * From https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/abcorr.html: - * - * Let r be the light time corrected vector from the observer to the object, and v be the velocity - * of the observer with respect to the solar system barycenter. Let w be the angle between them. - * The aberration angle phi is given by - * - *
    -                 sin(phi) = v sin(w)
    -                            --------
    -                            c
    -   * 
    - * - * Let h be the vector given by the cross product - * - *
    -                 h = r X v
    -   * 
    - * - * Rotate r by phi radians about h to obtain the apparent position of the object. - * - *

    - * Also see stelab.html - * - * @param targetLT - * @param observerVelocity - * @return - * @throws SpiceException - */ - public static Vector3 stelab(Vector3 targetLT, Vector3 observerVelocity) throws SpiceException { - Vector3 axis = targetLT.cross(observerVelocity); - double w = targetLT.sep(observerVelocity); - double phi = - FastMath.asin(observerVelocity.norm() * FastMath.sin(w) / PhysicalConstants.CLIGHT); - Matrix33 m = new AxisAndAngle(axis, phi).toMatrix(); - Vector3 aberratedVector = m.mxv(targetLT); - return aberratedVector; - } + TDBDuration lightTime = new TDBDuration(targetLTS.norm() / PhysicalConstants.CLIGHT); + TDBTime tmlt = t.sub(lightTime); - private DifferentiableUnivariateFunction dfunc(Vector3 targetLTS, Vector3 observerVelocity, - double step) { + // geometric states of target and observer + StateRecord targetState = new StateRecord(target, tmlt, J2000, NONE, SSB); + StateRecord observerState = new StateRecord(observer, t, J2000, NONE, SSB); - Vector3 axis = targetLTS.cross(observerVelocity); + // estimate aberration magnitude + StateRecord spiceLT = new StateRecord(target, t, J2000, new AberrationCorrection("LT"), observer); + StateRecord spiceLTS = new StateRecord(target, t, J2000, new AberrationCorrection("LT+S"), observer); - UnivariateFunction func = new UnivariateFunction() { + double angle = spiceLT.getPosition().sep(spiceLTS.getPosition()); + double step = angle / 10; - /** - * Rotate the targetLTS vector by angle. Treat this as a light-time corrected position. - * Correct for the aberration due to this position and compare to targetLTS. - *

    - * This function evaluates to zero if the two vectors agree. Rotate the LT+S vector by the - * input angle to get the LT vector. - */ - @Override - public double evaluate(double angle) { - try { - Matrix33 m = new AxisAndAngle(axis, angle).toMatrix(); - Vector3 targetLT = m.mxv(targetLTS); - Vector3 aberratedVector = stelab(targetLT, observerVelocity); - double sign = Math.signum(aberratedVector.cross(targetLTS).dot(axis)); + DifferentiableUnivariateFunction func = dfunc(targetLTS, observerState.getVelocity(), step); - return sign * aberratedVector.sep(targetLTS); - } catch (SpiceException e) { - logger.error(e.getLocalizedMessage(), e); - return Double.NaN; + RootFinder finder = RootFinder.create(); + Stepper stepper = Steppers.createConstant(step); + + IntervalSet boundingInterval = IntervalSet.create(new UnwritableInterval(-2 * angle, 2 * angle)); + + IntervalSet angle0 = finder.locateValue(func, 0., boundingInterval, stepper); + + Vector3 geometricObsToTarget = new Vector3(); + if (angle0.size() > 0) { + + Vector3 axis = targetLTS.cross(observerState.getVelocity()); + Matrix33 m = new AxisAndAngle(axis, angle0.get(0).getBegin()).toMatrix(); + + // this is target - observer + Vector3 targetLT = m.mxv(targetLTS); + + // targetState was evaluated at t - lt. This is the observer position relative to the SSB. + Vector3 observerPos = targetState.getPosition().sub(targetLT); + targetState = new StateRecord(target, t, J2000, NONE, SSB); + geometricObsToTarget = targetState.getPosition().sub(observerPos); + } else { + logger.error(this.getClass().getSimpleName() + ": no solution"); + System.exit(0); } - } - }; + return geometricObsToTarget; + } - return DifferentiableUnivariateFunctions.quadraticApproximation(func, step); + /** + * From https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/abcorr.html: + * + * Let r be the light time corrected vector from the observer to the object, and v be the velocity + * of the observer with respect to the solar system barycenter. Let w be the angle between them. + * The aberration angle phi is given by + * + *

    +     * sin(phi) = v sin(w)
    +     * --------
    +     * c
    +     * 
    + * + * Let h be the vector given by the cross product + * + *
    +     * h = r X v
    +     * 
    + * + * Rotate r by phi radians about h to obtain the apparent position of the object. + * + *

    + * Also see stelab.html + * + * @param targetLT + * @param observerVelocity + * @return + * @throws SpiceException + */ + public static Vector3 stelab(Vector3 targetLT, Vector3 observerVelocity) throws SpiceException { + Vector3 axis = targetLT.cross(observerVelocity); + double w = targetLT.sep(observerVelocity); + double phi = FastMath.asin(observerVelocity.norm() * FastMath.sin(w) / PhysicalConstants.CLIGHT); + Matrix33 m = new AxisAndAngle(axis, phi).toMatrix(); + Vector3 aberratedVector = m.mxv(targetLT); + return aberratedVector; + } - } + private DifferentiableUnivariateFunction dfunc(Vector3 targetLTS, Vector3 observerVelocity, double step) { + Vector3 axis = targetLTS.cross(observerVelocity); + + UnivariateFunction func = new UnivariateFunction() { + + /** + * Rotate the targetLTS vector by angle. Treat this as a light-time corrected position. + * Correct for the aberration due to this position and compare to targetLTS. + *

    + * This function evaluates to zero if the two vectors agree. Rotate the LT+S vector by the + * input angle to get the LT vector. + */ + @Override + public double evaluate(double angle) { + try { + Matrix33 m = new AxisAndAngle(axis, angle).toMatrix(); + Vector3 targetLT = m.mxv(targetLTS); + Vector3 aberratedVector = stelab(targetLT, observerVelocity); + double sign = Math.signum(aberratedVector.cross(targetLTS).dot(axis)); + + return sign * aberratedVector.sep(targetLTS); + } catch (SpiceException e) { + logger.error(e.getLocalizedMessage(), e); + return Double.NaN; + } + } + }; + + return DifferentiableUnivariateFunctions.quadraticApproximation(func, step); + } } diff --git a/src/main/java/terrasaur/utils/ResourceUtils.java b/src/main/java/terrasaur/utils/ResourceUtils.java index f1fb8c7..1543d2c 100644 --- a/src/main/java/terrasaur/utils/ResourceUtils.java +++ b/src/main/java/terrasaur/utils/ResourceUtils.java @@ -44,114 +44,115 @@ import org.apache.logging.log4j.Logger; /** * Methods to work with resource files stored inside a jar. - * + * * @author Hari.Nair@jhuapl.edu * */ public class ResourceUtils { - private final static Logger logger = LogManager.getLogger(ResourceUtils.class); + private static final Logger logger = LogManager.getLogger(ResourceUtils.class); - /** - * Write a jar resource to a specified file. - * - * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) - * @param file file to hold the resource. - * @param deleteOnExit delete file on program exit - * @return - */ - public static File writeResourceToFile(String path, File file, boolean deleteOnExit) { - URL input = ResourceUtils.class.getResource(path); - try { - FileUtils.copyURLToFile(input, file); - if (deleteOnExit) - file.deleteOnExit(); - } catch (IOException e) { - logger.warn(e.getLocalizedMessage()); - } - return file; - } - - /** - * Write a jar resource to a specified file. Calls - * {@link #writeResourceToFile(String, File, boolean)} with deleteOnExit set to - * {@link Boolean#TRUE}. - * - * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) - * @param file file to hold the resource. - * @return - */ - public static File writeResourceToFile(String path, File file) { - return writeResourceToFile(path, file, true); - } - - /** - * Write a jar resource to a temporary file. The file will be deleted on program exit. - * - * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) - * @return pointer to file created - */ - public static File writeResourceToFile(String path) { - - try { - return writeResourceToFile(path, File.createTempFile("resource-", ".tmp")); - } catch (IOException e) { - logger.warn(e.getLocalizedMessage()); - } - return null; - } - - /** - * Return a {@link List} of all Paths contained within the desired resource (e.g. /targets) - * - * @param path resource path to search - * @return paths under the resource path - */ - public static List getResourcePaths(String path) { - List paths = new ArrayList<>(); - try { - - URI uri = ResourceUtils.class.getResource(path).toURI(); - if (uri.getScheme().equals("jar")) { - - /*- - From https://mkyong.com/java/java-read-a-file-from-resources-folder/ - */ - - String jarPath = ResourceUtils.class.getProtectionDomain().getCodeSource().getLocation() - .toURI().getPath(); - - uri = URI.create("jar:file:" + jarPath); - try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { - paths = Files.walk(fs.getPath(path)).filter(Files::isRegularFile) - .collect(Collectors.toList()); + /** + * Write a jar resource to a specified file. + * + * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) + * @param file file to hold the resource. + * @param deleteOnExit delete file on program exit + * @return + */ + public static File writeResourceToFile(String path, File file, boolean deleteOnExit) { + URL input = ResourceUtils.class.getResource(path); + try { + FileUtils.copyURLToFile(input, file); + if (deleteOnExit) file.deleteOnExit(); + } catch (IOException e) { + logger.warn(e.getLocalizedMessage()); } - - } else { - Path myPath = Paths.get(uri); - - Stream walk = Files.walk(myPath, 1); - for (Iterator it = walk.iterator(); it.hasNext();) { - Path p = it.next(); - Path relative = myPath.relativize(p); - if (relative.toString().trim().length() == 0) - continue; - Path thisPath = Paths.get(path, relative.toString()); - if (Files.isDirectory(p)) { - paths.addAll(getResourcePaths(thisPath.toString())); - } else { - paths.add(thisPath); - } - } - walk.close(); - } - Collections.sort(paths); - } catch (URISyntaxException | IOException e) { - e.printStackTrace(); + return file; } - return paths; - } + /** + * Write a jar resource to a specified file. Calls + * {@link #writeResourceToFile(String, File, boolean)} with deleteOnExit set to + * {@link Boolean#TRUE}. + * + * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) + * @param file file to hold the resource. + * @return + */ + public static File writeResourceToFile(String path, File file) { + return writeResourceToFile(path, file, true); + } + /** + * Write a jar resource to a temporary file. The file will be deleted on program exit. + * + * @param path path to resource (e.g. /resources/kernels/lsk/naif0012.tls) + * @return pointer to file created + */ + public static File writeResourceToFile(String path) { + + try { + return writeResourceToFile(path, File.createTempFile("resource-", ".tmp")); + } catch (IOException e) { + logger.warn(e.getLocalizedMessage()); + } + return null; + } + + /** + * Return a {@link List} of all Paths contained within the desired resource (e.g. /targets) + * + * @param path resource path to search + * @return paths under the resource path + */ + public static List getResourcePaths(String path) { + List paths = new ArrayList<>(); + try { + + URI uri = ResourceUtils.class.getResource(path).toURI(); + if (uri.getScheme().equals("jar")) { + + /*- + From https://mkyong.com/java/java-read-a-file-from-resources-folder/ + */ + + String jarPath = ResourceUtils.class + .getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI() + .getPath(); + + uri = URI.create("jar:file:" + jarPath); + try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { + paths = Files.walk(fs.getPath(path)) + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + } + + } else { + Path myPath = Paths.get(uri); + + Stream walk = Files.walk(myPath, 1); + for (Iterator it = walk.iterator(); it.hasNext(); ) { + Path p = it.next(); + Path relative = myPath.relativize(p); + if (relative.toString().trim().length() == 0) continue; + Path thisPath = Paths.get(path, relative.toString()); + if (Files.isDirectory(p)) { + paths.addAll(getResourcePaths(thisPath.toString())); + } else { + paths.add(thisPath); + } + } + walk.close(); + } + Collections.sort(paths); + } catch (URISyntaxException | IOException e) { + e.printStackTrace(); + } + return paths; + } } diff --git a/src/main/java/terrasaur/utils/SBMTEllipseRecord.java b/src/main/java/terrasaur/utils/SBMTEllipseRecord.java index 22a976b..4a7ee5a 100644 --- a/src/main/java/terrasaur/utils/SBMTEllipseRecord.java +++ b/src/main/java/terrasaur/utils/SBMTEllipseRecord.java @@ -29,98 +29,111 @@ import terrasaur.utils.ImmutableSBMTEllipseRecord.Builder; @Value.Immutable public abstract class SBMTEllipseRecord { - abstract int id(); + abstract int id(); - abstract String name(); + abstract String name(); - public abstract double x(); + public abstract double x(); - public abstract double y(); + public abstract double y(); - public abstract double z(); + public abstract double z(); - abstract double lat(); + abstract double lat(); - abstract double lon(); + abstract double lon(); - abstract double radius(); + abstract double radius(); - abstract double slope(); + abstract double slope(); - abstract double elevation(); + abstract double elevation(); - abstract double acceleration(); + abstract double acceleration(); - abstract double potential(); + abstract double potential(); - abstract double diameter(); + abstract double diameter(); - abstract double flattening(); + abstract double flattening(); - abstract double angle(); + abstract double angle(); - abstract Color color(); + abstract Color color(); - abstract String dummy(); + abstract String dummy(); - abstract String label(); + abstract String label(); - public static SBMTEllipseRecord fromString(String string) { - String[] parts = string.split("\\s+"); + public static SBMTEllipseRecord fromString(String string) { + String[] parts = string.split("\\s+"); - int id = Integer.parseInt(parts[0]); - String name = parts[1]; - double x = Double.parseDouble(parts[2]); - double y = Double.parseDouble(parts[3]); - double z = Double.parseDouble(parts[4]); - double lat = Double.parseDouble(parts[5]); - double lon = Double.parseDouble(parts[6]); - double radius = Double.parseDouble(parts[7]); - double slope = Double.parseDouble(parts[8]); - double elevation = Double.parseDouble(parts[9]); - double acceleration = Double.parseDouble(parts[10]); - double potential = Double.parseDouble(parts[11]); - double diameter = Double.parseDouble(parts[12]); - double flattening = Double.parseDouble(parts[13]); - double angle = Double.parseDouble(parts[14]); - String[] colorParts = parts[15].split(","); - Color color = new Color(Integer.parseInt(colorParts[0]), Integer.parseInt(colorParts[1]), - Integer.parseInt(colorParts[2])); - String dummy = parts[16]; - StringBuilder label = new StringBuilder(); - for (int i = 17; i < parts.length; i++) - label.append(parts[i] + " "); + int id = Integer.parseInt(parts[0]); + String name = parts[1]; + double x = Double.parseDouble(parts[2]); + double y = Double.parseDouble(parts[3]); + double z = Double.parseDouble(parts[4]); + double lat = Double.parseDouble(parts[5]); + double lon = Double.parseDouble(parts[6]); + double radius = Double.parseDouble(parts[7]); + double slope = Double.parseDouble(parts[8]); + double elevation = Double.parseDouble(parts[9]); + double acceleration = Double.parseDouble(parts[10]); + double potential = Double.parseDouble(parts[11]); + double diameter = Double.parseDouble(parts[12]); + double flattening = Double.parseDouble(parts[13]); + double angle = Double.parseDouble(parts[14]); + String[] colorParts = parts[15].split(","); + Color color = new Color( + Integer.parseInt(colorParts[0]), Integer.parseInt(colorParts[1]), Integer.parseInt(colorParts[2])); + String dummy = parts[16]; + StringBuilder label = new StringBuilder(); + for (int i = 17; i < parts.length; i++) label.append(parts[i] + " "); - Builder record = ImmutableSBMTEllipseRecord.builder().id(id).name(name).x(x).y(y).z(z).lat(lat) - .lon(lon).radius(radius).slope(slope).elevation(elevation).acceleration(acceleration) - .potential(potential).diameter(diameter).flattening(flattening).angle(angle).color(color) - .dummy(dummy).label(label.toString().replace("\"", "").trim()); + Builder record = ImmutableSBMTEllipseRecord.builder() + .id(id) + .name(name) + .x(x) + .y(y) + .z(z) + .lat(lat) + .lon(lon) + .radius(radius) + .slope(slope) + .elevation(elevation) + .acceleration(acceleration) + .potential(potential) + .diameter(diameter) + .flattening(flattening) + .angle(angle) + .color(color) + .dummy(dummy) + .label(label.toString().replace("\"", "").trim()); - return record.build(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%d\t", id())); - sb.append(String.format("%s\t", name())); - sb.append(String.format("%s\t", Double.valueOf(x()).toString())); - sb.append(String.format("%s\t", Double.valueOf(y()).toString())); - sb.append(String.format("%s\t", Double.valueOf(z()).toString())); - sb.append(String.format("%s\t", Double.valueOf(lat()).toString())); - sb.append(String.format("%s\t", Double.valueOf(lon()).toString())); - sb.append(String.format("%s\t", Double.valueOf(radius()).toString())); - sb.append(String.format("%s\t", Double.valueOf(slope()).toString())); - sb.append(String.format("%s\t", Double.valueOf(elevation()).toString())); - sb.append(String.format("%s\t", Double.valueOf(acceleration()).toString())); - sb.append(String.format("%s\t", Double.valueOf(potential()).toString())); - sb.append(String.format("%s\t", Double.valueOf(diameter()).toString())); - sb.append(String.format("%s\t", Double.valueOf(flattening()).toString())); - sb.append(String.format("%s\t", Double.valueOf(angle()).toString())); - sb.append(String.format("%d,%d,%d\t", color().getRed(), color().getGreen(), color().getBlue())); - sb.append(String.format("%s\t", dummy())); - sb.append(String.format("\"%s\" ", label())); - return sb.toString(); - } + return record.build(); + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%d\t", id())); + sb.append(String.format("%s\t", name())); + sb.append(String.format("%s\t", Double.valueOf(x()).toString())); + sb.append(String.format("%s\t", Double.valueOf(y()).toString())); + sb.append(String.format("%s\t", Double.valueOf(z()).toString())); + sb.append(String.format("%s\t", Double.valueOf(lat()).toString())); + sb.append(String.format("%s\t", Double.valueOf(lon()).toString())); + sb.append(String.format("%s\t", Double.valueOf(radius()).toString())); + sb.append(String.format("%s\t", Double.valueOf(slope()).toString())); + sb.append(String.format("%s\t", Double.valueOf(elevation()).toString())); + sb.append(String.format("%s\t", Double.valueOf(acceleration()).toString())); + sb.append(String.format("%s\t", Double.valueOf(potential()).toString())); + sb.append(String.format("%s\t", Double.valueOf(diameter()).toString())); + sb.append(String.format("%s\t", Double.valueOf(flattening()).toString())); + sb.append(String.format("%s\t", Double.valueOf(angle()).toString())); + sb.append(String.format("%d,%d,%d\t", color().getRed(), color().getGreen(), color().getBlue())); + sb.append(String.format("%s\t", dummy())); + sb.append(String.format("\"%s\" ", label())); + return sb.toString(); + } } diff --git a/src/main/java/terrasaur/utils/SPICEUtil.java b/src/main/java/terrasaur/utils/SPICEUtil.java index 8853855..bfafa73 100644 --- a/src/main/java/terrasaur/utils/SPICEUtil.java +++ b/src/main/java/terrasaur/utils/SPICEUtil.java @@ -48,144 +48,139 @@ import spice.basic.Vector3; */ public class SPICEUtil { - public final static Comparator tdbComparator = new Comparator() { + public static final Comparator tdbComparator = new Comparator() { - @Override - public int compare(TDBTime o1, TDBTime o2) { - int compare = 0; - try { - compare = Double.compare(o1.getTDBSeconds(), o2.getTDBSeconds()); - } catch (SpiceErrorException e) { - e.printStackTrace(); - } - return compare; + @Override + public int compare(TDBTime o1, TDBTime o2) { + int compare = 0; + try { + compare = Double.compare(o1.getTDBSeconds(), o2.getTDBSeconds()); + } catch (SpiceErrorException e) { + e.printStackTrace(); + } + return compare; + } + }; + + /** + * Return the body fixed frame for a given body + * + * @param b + * @return + * @throws SpiceException + */ + public static ReferenceFrame getBodyFixedFrame(Body b) throws SpiceException { + + String frameKey = "OBJECT_" + b.getIDCode() + "_FRAME"; + if (!KernelPool.exists(frameKey)) { + frameKey = "OBJECT_" + b.getName() + "_FRAME"; + if (!KernelPool.exists(frameKey)) { + return null; + } + } + + ReferenceFrame bodyFixed; + + switch (KernelPool.getDataType(frameKey)) { + case KernelVarDescriptor.CHARACTER: + bodyFixed = new ReferenceFrame(KernelPool.getCharacter(frameKey)[0]); + break; + case KernelVarDescriptor.NUMERIC: + bodyFixed = new ReferenceFrame(KernelPool.getInteger(frameKey)[0]); + break; + default: + bodyFixed = null; + } + + return bodyFixed; } - }; + /** + * Check if a ray from the instrument origin in a specified direction is in the FOV. + * + * @param fov + * @param directionVector specified in instrument frame + * @return + * @throws SpiceErrorException + */ + public static List isInFOV(FOV fov, List directionVector) throws SpiceException { + List isInFOV = new ArrayList(); - /** - * Return the body fixed frame for a given body - * - * @param b - * @return - * @throws SpiceException - */ - public static ReferenceFrame getBodyFixedFrame(Body b) throws SpiceException { + if (fov.getShape().equals("RECTANGLE") || fov.getShape().equals("POLYGON")) { + Plane fovPlane = new Plane(fov.getBoresight(), fov.getBoresight()); + Vector3 origin = fovPlane.getPoint(); + Vector3 xAxis = fovPlane.getSpanningVectors()[0]; + Vector3 yAxis = fovPlane.getSpanningVectors()[1]; - String frameKey = "OBJECT_" + b.getIDCode() + "_FRAME"; - if (!KernelPool.exists(frameKey)) { - frameKey = "OBJECT_" + b.getName() + "_FRAME"; - if (!KernelPool.exists(frameKey)) { + Vector3[] boundaries = fov.getBoundary(); + Vector points = new Vector(); + for (Vector3 boundary : boundaries) { + RayPlaneIntercept rpi = new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), boundary), fovPlane); + points.add(rpi.getIntercept().sub(origin)); + } + + Path2D.Double shape = new Path2D.Double(); + shape.moveTo(xAxis.dot(points.get(0)), yAxis.dot(points.get(0))); + 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))); + + for (Vector3 direction : directionVector) { + RayPlaneIntercept rpi = new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), direction), fovPlane); + Vector3 pointOnPlane = rpi.getIntercept().sub(origin); + isInFOV.add(shape.contains(xAxis.dot(pointOnPlane), yAxis.dot(pointOnPlane))); + } + } else if (fov.getShape().equals("ELLIPSE")) { + throw new SpiceErrorException("FOV shape ELLIPSE not supported."); + } else if (fov.getShape().equals("CIRCLE")) { + Vector3 boundary = fov.getBoundary()[0]; + double fovRadius = Math.acos(fov.getBoresight().hat().dot(boundary.hat())); + for (Vector3 direction : directionVector) { + double dist = Math.acos(fov.getBoresight().hat().dot(direction.hat())); + isInFOV.add(dist < fovRadius); + } + } else { + throw new SpiceErrorException("Unknown FOV shape: " + fov.getShape()); + } + + return isInFOV; + } + + public static boolean isInFOV(FOV fov, Vector3 direction) throws SpiceException { + List directionVector = new ArrayList(); + directionVector.add(direction); + List isInFOV = isInFOV(fov, directionVector); + return isInFOV.get(0); + } + + public static Plane planeFromFacet(double[] a, double[] b, double[] c) { + Vector3 va = new Vector3(a); + Vector3 vab = new Vector3(b).sub(va); + Vector3 vac = new Vector3(c).sub(va); + + Plane p = null; + try { + p = new Plane(va, vab, vac); + } catch (SpiceException e) { + e.printStackTrace(); + } + return p; + } + + /** + * Return the inverse quaternion + * + * @param q + * @return + */ + public static double[] invertQuaternion(double[] q) { + try { + SpiceQuaternion sq = new SpiceQuaternion(q); + Matrix33 m = sq.toMatrix().invert(); + sq = new SpiceQuaternion(m); + return sq.toArray(); + } catch (SpiceException e) { + e.printStackTrace(); + } return null; - } } - - ReferenceFrame bodyFixed; - - switch (KernelPool.getDataType(frameKey)) { - case KernelVarDescriptor.CHARACTER: - bodyFixed = new ReferenceFrame(KernelPool.getCharacter(frameKey)[0]); - break; - case KernelVarDescriptor.NUMERIC: - bodyFixed = new ReferenceFrame(KernelPool.getInteger(frameKey)[0]); - break; - default: - bodyFixed = null; - } - - return bodyFixed; - } - - /** - * Check if a ray from the instrument origin in a specified direction is in the FOV. - * - * @param fov - * @param directionVector specified in instrument frame - * @return - * @throws SpiceErrorException - */ - public static List isInFOV(FOV fov, List directionVector) - throws SpiceException { - List isInFOV = new ArrayList(); - - if (fov.getShape().equals("RECTANGLE") || fov.getShape().equals("POLYGON")) { - Plane fovPlane = new Plane(fov.getBoresight(), fov.getBoresight()); - Vector3 origin = fovPlane.getPoint(); - Vector3 xAxis = fovPlane.getSpanningVectors()[0]; - Vector3 yAxis = fovPlane.getSpanningVectors()[1]; - - Vector3[] boundaries = fov.getBoundary(); - Vector points = new Vector(); - for (Vector3 boundary : boundaries) { - RayPlaneIntercept rpi = - new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), boundary), fovPlane); - points.add(rpi.getIntercept().sub(origin)); - } - - Path2D.Double shape = new Path2D.Double(); - shape.moveTo(xAxis.dot(points.get(0)), yAxis.dot(points.get(0))); - 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))); - - for (Vector3 direction : directionVector) { - RayPlaneIntercept rpi = - new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), direction), fovPlane); - Vector3 pointOnPlane = rpi.getIntercept().sub(origin); - isInFOV.add(shape.contains(xAxis.dot(pointOnPlane), yAxis.dot(pointOnPlane))); - } - } else if (fov.getShape().equals("ELLIPSE")) { - throw new SpiceErrorException("FOV shape ELLIPSE not supported."); - } else if (fov.getShape().equals("CIRCLE")) { - Vector3 boundary = fov.getBoundary()[0]; - double fovRadius = Math.acos(fov.getBoresight().hat().dot(boundary.hat())); - for (Vector3 direction : directionVector) { - double dist = Math.acos(fov.getBoresight().hat().dot(direction.hat())); - isInFOV.add(dist < fovRadius); - } - } else { - throw new SpiceErrorException("Unknown FOV shape: " + fov.getShape()); - } - - return isInFOV; - } - - public static boolean isInFOV(FOV fov, Vector3 direction) throws SpiceException { - List directionVector = new ArrayList(); - directionVector.add(direction); - List isInFOV = isInFOV(fov, directionVector); - return isInFOV.get(0); - } - - public static Plane planeFromFacet(double[] a, double[] b, double[] c) { - Vector3 va = new Vector3(a); - Vector3 vab = new Vector3(b).sub(va); - Vector3 vac = new Vector3(c).sub(va); - - Plane p = null; - try { - p = new Plane(va, vab, vac); - } catch (SpiceException e) { - e.printStackTrace(); - } - return p; - } - - /** - * Return the inverse quaternion - * - * @param q - * @return - */ - public static double[] invertQuaternion(double[] q) { - try { - SpiceQuaternion sq = new SpiceQuaternion(q); - Matrix33 m = sq.toMatrix().invert(); - sq = new SpiceQuaternion(m); - return sq.toArray(); - } catch (SpiceException e) { - e.printStackTrace(); - } - return null; - } } diff --git a/src/main/java/terrasaur/utils/StringUtil.java b/src/main/java/terrasaur/utils/StringUtil.java index 362cbd0..3b4be25 100644 --- a/src/main/java/terrasaur/utils/StringUtil.java +++ b/src/main/java/terrasaur/utils/StringUtil.java @@ -22,15 +22,13 @@ */ package terrasaur.utils; -import picante.math.coords.LatitudinalVector; - import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Pattern; - +import picante.math.coords.LatitudinalVector; /** * Contains static list of string utilities. @@ -39,360 +37,359 @@ import java.util.regex.Pattern; */ public class StringUtil { - // From - // https://stackoverflow.com/questions/523871/best-way-to-concatenate-list-of-string-objects - public static String concatStringsWSep(List strings, String separator) { - StringBuilder sb = new StringBuilder(); - String sep = ""; - for (Object s : strings) { - if (s == null) sb.append(sep).append("NA"); - else sb.append(sep).append(s); - sep = separator; - } - return sb.toString(); - } - - // From - // https://stackoverflow.com/questions/523871/best-way-to-concatenate-list-of-string-objects - public static String concatDToStringsWSep( - List dataValues, String formatD, String separator, String nanString) { - StringBuilder sb = new StringBuilder(); - String sep = ""; - for (Double dToConvert : dataValues) { - - if (dToConvert == null) sb.append(sep).append(nanString); - else { - - // need to check string conversion for each value to see if it is valid - // String formatD = "% 30.16f"; - String testConvert = String.format(formatD, dToConvert); - double checkVal = StringUtil.parseSafeD(testConvert); - if (Double.isNaN(checkVal)) { - - // System.out.println("Error converting dToConvert to string! string:" + testConvert); - // System.out.println("Writing " + nanString + " instead."); - sb.append(sep).append(nanString); - - } else { - sb.append(sep).append(testConvert); + // From + // https://stackoverflow.com/questions/523871/best-way-to-concatenate-list-of-string-objects + public static String concatStringsWSep(List strings, String separator) { + StringBuilder sb = new StringBuilder(); + String sep = ""; + for (Object s : strings) { + if (s == null) sb.append(sep).append("NA"); + else sb.append(sep).append(s); + sep = separator; } - } - sep = separator; - } - return sb.toString(); - } - - /** - * Format a double according to the specified string format, then convert back to a double - * - * @param formatS - regex string describing format of rawD eg. "%10.3f" - * @param rawD - double being formatted - * @return - */ - public static double formatD(String formatS, double rawD) { - - String doubleS = String.format(formatS, rawD); - double newD = Double.parseDouble(doubleS); - return newD; - } - - /** - * Convert string value to a double, formatted according to the specified string format. - * - * @param formatS - * @param value - * @return - */ - public static double str2fmtD(String formatS, String value) { - - double dVal = parseSafeD(value); - if (dVal != Double.NaN) { - return formatD(formatS, dVal); - } else { - return Double.NaN; - } - } - - /** - * convert from string to double. Does testing to determine if string represents a valid double. - * If not then returns Double.NaN; - * - * @param myString - * @return - */ - public static double parseSafeD(String myString) { - // final String Digits = "(\\p{Digit}+)"; - // final String HexDigits = "(\\p{XDigit}+)"; - // // an exponent is 'e' or 'E' followed by an optionally - // // signed decimal integer. - // final String Exp = "[eE][+-]?" + Digits; - // final String fpRegex = ("[\\x00-\\x20]*" + // Optional leading "whitespace" - // "[+-]?(" + // Optional sign character - // "NaN|" + // "NaN" string - // "Infinity|" + // "Infinity" string - // - // // A decimal floating-point string representing a finite positive - // // number without a leading sign has at most five basic pieces: - // // Digits . Digits ExponentPart FloatTypeSuffix - // // - // // Since this method allows integer-only strings as input - // // in addition to strings of floating-point literals, the - // // two sub-patterns below are simplifications of the grammar - // // productions from the Java Language Specification, 2nd - // // edition, section 3.10.2. - // - // // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt - // "(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|" + - // - // // . Digits ExponentPart_opt FloatTypeSuffix_opt - // "(\\.(" + Digits + ")(" + Exp + ")?)|" + - // - // // Hexadecimal strings - // "((" + - // // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt - // "(0[xX]" + HexDigits + "(\\.)?)|" + - // - // // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt - // "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")" + - // - // ")[pP][+-]?" + Digits + "))" + - // "[fFdD]?))" + - // "[\\x00-\\x20]*");// Optional trailing "whitespace" - // - // if (Pattern.matches(fpRegex, myString)) { - // return Double.valueOf(myString); // Will not throw NumberFormatException - // } else { - // // Perform suitable alternative action - // return Double.NaN; - // } - - boolean throwException = false; - return parseSafeDException(myString, throwException); - } - - /** - * Allow user to control error handling. If throwException is true then method will throw a - * runtimeException when it cannot parse a valid double from myString. - * - * @param myString - * @param throwException - * @return - */ - public static double parseSafeDException(String myString, boolean throwException) { - - final String Digits = "(\\p{Digit}+)"; - final String HexDigits = "(\\p{XDigit}+)"; - // an exponent is 'e' or 'E' followed by an optionally - // signed decimal integer. - final String Exp = "[eE][+-]?" + Digits; - final String fpRegex = - ("[\\x00-\\x20]*" - + // Optional leading "whitespace" - "[+-]?(" - + // Optional sign character - "NaN|" - + // "NaN" string - "Infinity|" - + // "Infinity" string - - // A decimal floating-point string representing a finite positive - // number without a leading sign has at most five basic pieces: - // Digits . Digits ExponentPart FloatTypeSuffix - // - // Since this method allows integer-only strings as input - // in addition to strings of floating-point literals, the - // two sub-patterns below are simplifications of the grammar - // productions from the Java Language Specification, 2nd - // edition, section 3.10.2. - - // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt - "(((" - + Digits - + "(\\.)?(" - + Digits - + "?)(" - + Exp - + ")?)|" - + - - // . Digits ExponentPart_opt FloatTypeSuffix_opt - "(\\.(" - + Digits - + ")(" - + Exp - + ")?)|" - + - - // Hexadecimal strings - "((" - + - // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt - "(0[xX]" - + HexDigits - + "(\\.)?)|" - + - - // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt - "(0[xX]" - + HexDigits - + "?(\\.)" - + HexDigits - + ")" - + ")[pP][+-]?" - + Digits - + "))" - + "[fFdD]?))" - + "[\\x00-\\x20]*"); // Optional trailing - // "whitespace" - - if (Pattern.matches(fpRegex, myString)) { - return Double.valueOf(myString); // Will not throw NumberFormatException - } else { - // Perform suitable alternative action - if (throwException) { - String errMesg = "ERROR! Could not parse double from:" + myString; - throw new RuntimeException(errMesg); - } else { - return Double.NaN; - } - } - } - - public static Integer parseSafeInt(String numStr) { - try { - int num = Integer.parseInt(numStr); - return num; - } catch (NumberFormatException nfe) { - // not a number - String errMesg = "ERROR:" + numStr + "could not be" + "converted to an Integer!"; - throw new RuntimeException(errMesg); - } - } - - /** - * Create and return current date time in format: yyyyMMdd'T'hhMMss. Used for ALTWG PDS naming - * convention - * - * @return - */ - public static String currDTime() { - - DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmms"); - Date date = new Date(); - String cDTime = dateFormat.format(date); - return cDTime; - } - - /** - * pad with n spaces to the right of string - * - * @param s - * @param n - * @return - */ - public static String padRight(String s, int n) { - return String.format("%1$-" + n + "s", s); - } - - /** - * Pad with n spaces to left of string - * - * @param s - * @param n - * @return - */ - public static String padLeft(String s, int n) { - return String.format("%1$" + n + "s", s); - } - - /** Pad with spaces to right of string until length is n. WARNING: Will not truncate if s > n */ - public static String padRightToN(String s, int n) { - String newString; - if (s.length() < n) { - newString = padRight(s, n); - } else { - newString = s; - } - return newString; - } - - /** Pad with spaces to left of string until length is n. WARNING: Will not truncate if s > n */ - public static String padLeftToN(String s, int n) { - String newString; - if (s.length() < n) { - newString = padLeft(s, n); - } else { - newString = s; - } - return newString; - } - - /** - * Used to get current time in yyyy-mm-ddThh:mm:ss format Can be used for debugging statements or - * to fill out time keywords - * - * @return - */ - public static String timenow() { - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - Date date = new Date(); - String timenow = dateFormat.format(date); - return timenow; - } - - /** - * Strip newline characters from incoming string, then break into a list of strings with length no - * greater than n. - * - * @param originalString - * @param n - * @return - */ - public static List wrapString(String originalString, int n) { - List results = new ArrayList<>(); - - String[] parts = originalString.replaceAll("\\r|\\n", " ").split("\\s+"); - StringBuilder sb = new StringBuilder(); - for (String s : parts) { - if (sb.length() + s.length() >= n) { - results.add(sb.toString()); - sb = new StringBuilder(); - } - // replace multiple spaces with a single space - sb.append(s.trim().replaceAll("\\s{2,}", " ")); - sb.append(" "); + return sb.toString(); } - return results; - } + // From + // https://stackoverflow.com/questions/523871/best-way-to-concatenate-list-of-string-objects + public static String concatDToStringsWSep( + List dataValues, String formatD, String separator, String nanString) { + StringBuilder sb = new StringBuilder(); + String sep = ""; + for (Double dToConvert : dataValues) { - /** - * convert a string array to a single string. - * - * @param originalString - * @param insertNewLine insert newline characters at the end of each array element - * @return - */ - public static String flattenStringArray(String[] originalString, boolean insertNewLine) { - StringBuilder sb = new StringBuilder(); - for (String s : originalString) { - // replace multiple spaces with a single space - sb.append(s.trim().replaceAll("\\s{2,}", " ")); - sb.append(insertNewLine ? "\n" : " "); + if (dToConvert == null) sb.append(sep).append(nanString); + else { + + // need to check string conversion for each value to see if it is valid + // String formatD = "% 30.16f"; + String testConvert = String.format(formatD, dToConvert); + double checkVal = StringUtil.parseSafeD(testConvert); + if (Double.isNaN(checkVal)) { + + // System.out.println("Error converting dToConvert to string! string:" + testConvert); + // System.out.println("Writing " + nanString + " instead."); + sb.append(sep).append(nanString); + + } else { + sb.append(sep).append(testConvert); + } + } + sep = separator; + } + return sb.toString(); } - return sb.toString(); - } - /** - * Returns a string in the format 1200n08900 - * - * @param lv - * @return - */ - public static String lvToString(LatitudinalVector lv) { - int lat = (int) (Math.abs(Math.toDegrees(lv.getLatitude())) * 100 + 0.5); - int lon = (int) (Math.abs(Math.toDegrees(lv.getLongitude())) * 100 + 0.5); + /** + * Format a double according to the specified string format, then convert back to a double + * + * @param formatS - regex string describing format of rawD eg. "%10.3f" + * @param rawD - double being formatted + * @return + */ + public static double formatD(String formatS, double rawD) { - return String.format("%04d%s%05d", lat, lv.getLatitude() > 0 ? "n" : "s", lon); - } + String doubleS = String.format(formatS, rawD); + double newD = Double.parseDouble(doubleS); + return newD; + } + + /** + * Convert string value to a double, formatted according to the specified string format. + * + * @param formatS + * @param value + * @return + */ + public static double str2fmtD(String formatS, String value) { + + double dVal = parseSafeD(value); + if (dVal != Double.NaN) { + return formatD(formatS, dVal); + } else { + return Double.NaN; + } + } + + /** + * convert from string to double. Does testing to determine if string represents a valid double. + * If not then returns Double.NaN; + * + * @param myString + * @return + */ + public static double parseSafeD(String myString) { + // final String Digits = "(\\p{Digit}+)"; + // final String HexDigits = "(\\p{XDigit}+)"; + // // an exponent is 'e' or 'E' followed by an optionally + // // signed decimal integer. + // final String Exp = "[eE][+-]?" + Digits; + // final String fpRegex = ("[\\x00-\\x20]*" + // Optional leading "whitespace" + // "[+-]?(" + // Optional sign character + // "NaN|" + // "NaN" string + // "Infinity|" + // "Infinity" string + // + // // A decimal floating-point string representing a finite positive + // // number without a leading sign has at most five basic pieces: + // // Digits . Digits ExponentPart FloatTypeSuffix + // // + // // Since this method allows integer-only strings as input + // // in addition to strings of floating-point literals, the + // // two sub-patterns below are simplifications of the grammar + // // productions from the Java Language Specification, 2nd + // // edition, section 3.10.2. + // + // // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt + // "(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|" + + // + // // . Digits ExponentPart_opt FloatTypeSuffix_opt + // "(\\.(" + Digits + ")(" + Exp + ")?)|" + + // + // // Hexadecimal strings + // "((" + + // // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt + // "(0[xX]" + HexDigits + "(\\.)?)|" + + // + // // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt + // "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")" + + // + // ")[pP][+-]?" + Digits + "))" + + // "[fFdD]?))" + + // "[\\x00-\\x20]*");// Optional trailing "whitespace" + // + // if (Pattern.matches(fpRegex, myString)) { + // return Double.valueOf(myString); // Will not throw NumberFormatException + // } else { + // // Perform suitable alternative action + // return Double.NaN; + // } + + boolean throwException = false; + return parseSafeDException(myString, throwException); + } + + /** + * Allow user to control error handling. If throwException is true then method will throw a + * runtimeException when it cannot parse a valid double from myString. + * + * @param myString + * @param throwException + * @return + */ + public static double parseSafeDException(String myString, boolean throwException) { + + final String Digits = "(\\p{Digit}+)"; + final String HexDigits = "(\\p{XDigit}+)"; + // an exponent is 'e' or 'E' followed by an optionally + // signed decimal integer. + final String Exp = "[eE][+-]?" + Digits; + final String fpRegex = ("[\\x00-\\x20]*" + + // Optional leading "whitespace" + "[+-]?(" + + // Optional sign character + "NaN|" + + // "NaN" string + "Infinity|" + + // "Infinity" string + + // A decimal floating-point string representing a finite positive + // number without a leading sign has at most five basic pieces: + // Digits . Digits ExponentPart FloatTypeSuffix + // + // Since this method allows integer-only strings as input + // in addition to strings of floating-point literals, the + // two sub-patterns below are simplifications of the grammar + // productions from the Java Language Specification, 2nd + // edition, section 3.10.2. + + // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt + "(((" + + Digits + + "(\\.)?(" + + Digits + + "?)(" + + Exp + + ")?)|" + + + + // . Digits ExponentPart_opt FloatTypeSuffix_opt + "(\\.(" + + Digits + + ")(" + + Exp + + ")?)|" + + + + // Hexadecimal strings + "((" + + + // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt + "(0[xX]" + + HexDigits + + "(\\.)?)|" + + + + // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt + "(0[xX]" + + HexDigits + + "?(\\.)" + + HexDigits + + ")" + + ")[pP][+-]?" + + Digits + + "))" + + "[fFdD]?))" + + "[\\x00-\\x20]*"); // Optional trailing + // "whitespace" + + if (Pattern.matches(fpRegex, myString)) { + return Double.valueOf(myString); // Will not throw NumberFormatException + } else { + // Perform suitable alternative action + if (throwException) { + String errMesg = "ERROR! Could not parse double from:" + myString; + throw new RuntimeException(errMesg); + } else { + return Double.NaN; + } + } + } + + public static Integer parseSafeInt(String numStr) { + try { + int num = Integer.parseInt(numStr); + return num; + } catch (NumberFormatException nfe) { + // not a number + String errMesg = "ERROR:" + numStr + "could not be" + "converted to an Integer!"; + throw new RuntimeException(errMesg); + } + } + + /** + * Create and return current date time in format: yyyyMMdd'T'hhMMss. Used for ALTWG PDS naming + * convention + * + * @return + */ + public static String currDTime() { + + DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmms"); + Date date = new Date(); + String cDTime = dateFormat.format(date); + return cDTime; + } + + /** + * pad with n spaces to the right of string + * + * @param s + * @param n + * @return + */ + public static String padRight(String s, int n) { + return String.format("%1$-" + n + "s", s); + } + + /** + * Pad with n spaces to left of string + * + * @param s + * @param n + * @return + */ + public static String padLeft(String s, int n) { + return String.format("%1$" + n + "s", s); + } + + /** Pad with spaces to right of string until length is n. WARNING: Will not truncate if s > n */ + public static String padRightToN(String s, int n) { + String newString; + if (s.length() < n) { + newString = padRight(s, n); + } else { + newString = s; + } + return newString; + } + + /** Pad with spaces to left of string until length is n. WARNING: Will not truncate if s > n */ + public static String padLeftToN(String s, int n) { + String newString; + if (s.length() < n) { + newString = padLeft(s, n); + } else { + newString = s; + } + return newString; + } + + /** + * Used to get current time in yyyy-mm-ddThh:mm:ss format Can be used for debugging statements or + * to fill out time keywords + * + * @return + */ + public static String timenow() { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + Date date = new Date(); + String timenow = dateFormat.format(date); + return timenow; + } + + /** + * Strip newline characters from incoming string, then break into a list of strings with length no + * greater than n. + * + * @param originalString + * @param n + * @return + */ + public static List wrapString(String originalString, int n) { + List results = new ArrayList<>(); + + String[] parts = originalString.replaceAll("\\r|\\n", " ").split("\\s+"); + StringBuilder sb = new StringBuilder(); + for (String s : parts) { + if (sb.length() + s.length() >= n) { + results.add(sb.toString()); + sb = new StringBuilder(); + } + // replace multiple spaces with a single space + sb.append(s.trim().replaceAll("\\s{2,}", " ")); + sb.append(" "); + } + + return results; + } + + /** + * convert a string array to a single string. + * + * @param originalString + * @param insertNewLine insert newline characters at the end of each array element + * @return + */ + public static String flattenStringArray(String[] originalString, boolean insertNewLine) { + StringBuilder sb = new StringBuilder(); + for (String s : originalString) { + // replace multiple spaces with a single space + sb.append(s.trim().replaceAll("\\s{2,}", " ")); + sb.append(insertNewLine ? "\n" : " "); + } + return sb.toString(); + } + + /** + * Returns a string in the format 1200n08900 + * + * @param lv + * @return + */ + public static String lvToString(LatitudinalVector lv) { + int lat = (int) (Math.abs(Math.toDegrees(lv.getLatitude())) * 100 + 0.5); + int lon = (int) (Math.abs(Math.toDegrees(lv.getLongitude())) * 100 + 0.5); + + return String.format("%04d%s%05d", lat, lv.getLatitude() > 0 ? "n" : "s", lon); + } } diff --git a/src/main/java/terrasaur/utils/SumFile.java b/src/main/java/terrasaur/utils/SumFile.java index b4fda39..0e5762f 100644 --- a/src/main/java/terrasaur/utils/SumFile.java +++ b/src/main/java/terrasaur/utils/SumFile.java @@ -57,475 +57,434 @@ import terrasaur.utils.spice.SpiceBundle; @Value.Immutable public abstract class SumFile { - private static final Logger logger = LogManager.getLogger(SumFile.class); + private static final Logger logger = LogManager.getLogger(SumFile.class); - // line 1 - public abstract String picnm(); + // line 1 + public abstract String picnm(); - // line 2 - public abstract String utcString(); + // line 2 + public abstract String utcString(); - // line 3 - public abstract int npx(); + // line 3 + public abstract int npx(); - public abstract int nln(); + public abstract int nln(); - public abstract int t1(); + public abstract int t1(); - public abstract int t2(); + public abstract int t2(); - // line 4 - public abstract double mmfl(); + // line 4 + public abstract double mmfl(); - public abstract double px0(); + public abstract double px0(); - public abstract double ln0(); + public abstract double ln0(); - // line 5 - public abstract Vector3D scobj(); + // line 5 + public abstract Vector3D scobj(); - // line 6 - public abstract Vector3D cx(); + // line 6 + public abstract Vector3D cx(); - // line 7 - public abstract Vector3D cy(); + // line 7 + public abstract Vector3D cy(); - // line 8 - public abstract Vector3D cz(); + // line 8 + public abstract Vector3D cz(); - // line 9 - public abstract Vector3D sz(); + // line 9 + public abstract Vector3D sz(); - // line 10 - public abstract Vector3D kmat1(); + // line 10 + public abstract Vector3D kmat1(); - public abstract Vector3D kmat2(); + public abstract Vector3D kmat2(); - // line 11 - public abstract List distortion(); + // line 11 + public abstract List distortion(); - // line 12 - public abstract Vector3D sig_vso(); + // line 12 + public abstract Vector3D sig_vso(); - // line 13 - public abstract Vector3D sig_ptg(); + // line 13 + public abstract Vector3D sig_ptg(); - public abstract Vector3D frustum1(); + public abstract Vector3D frustum1(); - public abstract Vector3D frustum2(); + public abstract Vector3D frustum2(); - public abstract Vector3D frustum3(); + public abstract Vector3D frustum3(); - public abstract Vector3D frustum4(); + public abstract Vector3D frustum4(); - /** - * @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(); + /** + * @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); - - Vector3D scObj = rotation.applyTo(scobj()); - scObj = scObj.add(translation); - - b.scobj(scObj); - b.cx(rotation.applyTo(cx())); - b.cy(rotation.applyTo(cy())); - b.cz(rotation.applyTo(cz())); - b.sz(rotation.applyTo(sz())); - - return b.build(); - } - - /** - * @param file file to read - * @return SumFile - */ - public static SumFile fromFile(File file) { - SumFile s = null; - try { - s = fromLines(FileUtils.readLines(file, Charset.defaultCharset())); - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - return s; - } - - /** - * @param lines lines to read - * @return SumFile - */ - public static SumFile fromLines(List lines) { - Builder b = ImmutableSumFile.builder(); - b.picnm(lines.get(0).trim()); - b.utcString(lines.get(1).trim()); - - String[] parts = lines.get(2).trim().split("\\s+"); - b.npx(Integer.parseInt(parts[0])); - b.nln(Integer.parseInt(parts[1])); - b.t1(Integer.parseInt(parts[2])); - b.t2(Integer.parseInt(parts[3])); - - parts = lines.get(3).trim().split("\\s+"); - b.mmfl(parseFortranDouble(parts[0])); - b.px0(parseFortranDouble(parts[1])); - b.ln0(parseFortranDouble(parts[2])); - - parts = lines.get(4).trim().split("\\s+"); - double[] tmp = new double[3]; - 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]); - 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]); - 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]); - 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]); - 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]); - b.kmat1(new Vector3D(tmp)); - tmp = new double[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])); - - parts = lines.get(11).trim().split("\\s+"); - tmp = new double[3]; - 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]); - 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(); - 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(); - 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(); - 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(); - b.frustum2(new Vector3D(fx, fy, fz).normalize()); - - return b.build(); - } - - /** - * 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 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 */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%s\n", picnm())); - sb.append(String.format("%s\n", utcString())); - sb.append( - 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("%s\n", "LANDMARKS")); - sb.append(String.format("%s\n", "LIMB FITS")); - sb.append(String.format("%s\n", "END FILE")); - - return sb.toString(); - } - - /** - * @return Boresight direction. This is the same as {@link #cz()}. - */ - public Vector3D boresight() { - return cz(); - } - - /** - * @return Sun direction. This is the same as {@link #sz()}. - */ - public Vector3D sunDirection() { - return sz(); - } - - /** - * @return - *
    new Vector3D(1. / npx(), frustum3().subtract(frustum4()))
    - */ - public Vector3D xPerPixel() { - return new Vector3D(1. / npx(), frustum3().subtract(frustum4())); - } - - /** - * @return - *
     new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
    -   *        
    - */ - public Vector3D yPerPixel() { - return new Vector3D(1. / nln(), frustum3().subtract(frustum1())); - } - - /** - * @return Angular size per pixel in the X direction calculated using - *
    Vector3D.angle(frustum3(), frustum4()) / npx()
    - */ - public double horizontalResolution() { - return Vector3D.angle(frustum3(), frustum4()) / npx(); - } - - /** - * @return Angular size per pixel in the Y direction calculated using - *
    Vector3D.angle(frustum3(), frustum1()) / nln()
    - */ - public double verticalResolution() { - return Vector3D.angle(frustum3(), frustum1()) / nln(); - } - - /** - * @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)); - return imageHeight; - } - - /** - * @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)); - return imageWidth; - } - - /** - * @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][]; - m[0] = cx().toArray(); - m[1] = cy().toArray(); - m[2] = cz().toArray(); - return new Rotation(m, 1e-10); - } - - /** - * @param directions from the spacecraft, in the body fixed frame. - * @return list of booleans set to true if direction is in the field of view, or false if not - * @throws SpiceException - */ - public List isInFOV(List directions) throws SpiceException { - List isInFOV = new ArrayList<>(); - - Vector3 boresight = MathConversions.toVector3(boresight()); - Plane fovPlane = new Plane(boresight, boresight); - Vector3 origin = fovPlane.getPoint(); - Vector3 xAxis = fovPlane.getSpanningVectors()[0]; - Vector3 yAxis = fovPlane.getSpanningVectors()[1]; - - List boundaries = new ArrayList<>(); - boundaries.add(MathConversions.toVector3(frustum1())); - boundaries.add(MathConversions.toVector3(frustum2())); - boundaries.add(MathConversions.toVector3(frustum4())); - boundaries.add(MathConversions.toVector3(frustum3())); - List points = new ArrayList<>(); - for (Vector3 boundary : boundaries) { - RayPlaneIntercept rpi = - new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), boundary), fovPlane); - points.add(rpi.getIntercept().sub(origin)); + 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(); } - Path2D.Double shape = new Path2D.Double(); - 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.getFirst()), yAxis.dot(points.getFirst())); + /** + * @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); - for (Vector3 direction : directions) { - RayPlaneIntercept rpi = - new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), direction), fovPlane); - Vector3 pointOnPlane = rpi.getIntercept().sub(origin); - isInFOV.add(shape.contains(xAxis.dot(pointOnPlane), yAxis.dot(pointOnPlane))); + Vector3D scObj = rotation.applyTo(scobj()); + scObj = scObj.add(translation); + + b.scobj(scObj); + b.cx(rotation.applyTo(cx())); + b.cy(rotation.applyTo(cy())); + b.cz(rotation.applyTo(cz())); + b.sz(rotation.applyTo(sz())); + + return b.build(); } - return Collections.unmodifiableList(isInFOV); - } + /** + * @param file file to read + * @return SumFile + */ + public static SumFile fromFile(File file) { + SumFile s = null; + try { + s = fromLines(FileUtils.readLines(file, Charset.defaultCharset())); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + return s; + } + + /** + * @param lines lines to read + * @return SumFile + */ + public static SumFile fromLines(List lines) { + Builder b = ImmutableSumFile.builder(); + b.picnm(lines.get(0).trim()); + b.utcString(lines.get(1).trim()); + + String[] parts = lines.get(2).trim().split("\\s+"); + b.npx(Integer.parseInt(parts[0])); + b.nln(Integer.parseInt(parts[1])); + b.t1(Integer.parseInt(parts[2])); + b.t2(Integer.parseInt(parts[3])); + + parts = lines.get(3).trim().split("\\s+"); + b.mmfl(parseFortranDouble(parts[0])); + b.px0(parseFortranDouble(parts[1])); + b.ln0(parseFortranDouble(parts[2])); + + parts = lines.get(4).trim().split("\\s+"); + double[] tmp = new double[3]; + 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]); + 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]); + 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]); + 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]); + 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]); + b.kmat1(new Vector3D(tmp)); + tmp = new double[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])); + + parts = lines.get(11).trim().split("\\s+"); + tmp = new double[3]; + 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]); + 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(); + 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(); + 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(); + 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(); + b.frustum2(new Vector3D(fx, fy, fz).normalize()); + + return b.build(); + } + + /** + * 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 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 */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%s\n", picnm())); + sb.append(String.format("%s\n", utcString())); + sb.append(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("%s\n", "LANDMARKS")); + sb.append(String.format("%s\n", "LIMB FITS")); + sb.append(String.format("%s\n", "END FILE")); + + return sb.toString(); + } + + /** + * @return Boresight direction. This is the same as {@link #cz()}. + */ + public Vector3D boresight() { + return cz(); + } + + /** + * @return Sun direction. This is the same as {@link #sz()}. + */ + public Vector3D sunDirection() { + return sz(); + } + + /** + * @return + *
    new Vector3D(1. / npx(), frustum3().subtract(frustum4()))
    + */ + public Vector3D xPerPixel() { + return new Vector3D(1. / npx(), frustum3().subtract(frustum4())); + } + + /** + * @return + *
     new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
    +     *        
    + */ + public Vector3D yPerPixel() { + return new Vector3D(1. / nln(), frustum3().subtract(frustum1())); + } + + /** + * @return Angular size per pixel in the X direction calculated using + *
    Vector3D.angle(frustum3(), frustum4()) / npx()
    + */ + public double horizontalResolution() { + return Vector3D.angle(frustum3(), frustum4()) / npx(); + } + + /** + * @return Angular size per pixel in the Y direction calculated using + *
    Vector3D.angle(frustum3(), frustum1()) / nln()
    + */ + public double verticalResolution() { + return Vector3D.angle(frustum3(), frustum1()) / nln(); + } + + /** + * @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)); + return imageHeight; + } + + /** + * @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)); + return imageWidth; + } + + /** + * @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][]; + m[0] = cx().toArray(); + m[1] = cy().toArray(); + m[2] = cz().toArray(); + return new Rotation(m, 1e-10); + } + + /** + * @param directions from the spacecraft, in the body fixed frame. + * @return list of booleans set to true if direction is in the field of view, or false if not + * @throws SpiceException + */ + public List isInFOV(List directions) throws SpiceException { + List isInFOV = new ArrayList<>(); + + Vector3 boresight = MathConversions.toVector3(boresight()); + Plane fovPlane = new Plane(boresight, boresight); + Vector3 origin = fovPlane.getPoint(); + Vector3 xAxis = fovPlane.getSpanningVectors()[0]; + Vector3 yAxis = fovPlane.getSpanningVectors()[1]; + + List boundaries = new ArrayList<>(); + boundaries.add(MathConversions.toVector3(frustum1())); + boundaries.add(MathConversions.toVector3(frustum2())); + boundaries.add(MathConversions.toVector3(frustum4())); + boundaries.add(MathConversions.toVector3(frustum3())); + List points = new ArrayList<>(); + for (Vector3 boundary : boundaries) { + RayPlaneIntercept rpi = new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), boundary), fovPlane); + points.add(rpi.getIntercept().sub(origin)); + } + + Path2D.Double shape = new Path2D.Double(); + 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.getFirst()), yAxis.dot(points.getFirst())); + + for (Vector3 direction : directions) { + RayPlaneIntercept rpi = new RayPlaneIntercept(new Ray(new Vector3(0, 0, 0), direction), fovPlane); + Vector3 pointOnPlane = rpi.getIntercept().sub(origin); + isInFOV.add(shape.contains(xAxis.dot(pointOnPlane), yAxis.dot(pointOnPlane))); + } + + return Collections.unmodifiableList(isInFOV); + } } diff --git a/src/main/java/terrasaur/utils/Tilts.java b/src/main/java/terrasaur/utils/Tilts.java index 5428557..ee23675 100644 --- a/src/main/java/terrasaur/utils/Tilts.java +++ b/src/main/java/terrasaur/utils/Tilts.java @@ -29,35 +29,32 @@ import org.apache.commons.math3.util.FastMath; public class Tilts { - /** - * Basic tilt direction definition: Project plate normal into the plate facet. Determine angle in - * CW of projected facet assuming up or N is 0deg. Take longitude and generate a quaternion - * representing rotation about the Z-axis. Rotate normal vector by quaternion such that new - * coordinate system x' is now along R. Call this n'. - * - *
    -   * Tilt direction B = 90 - atan2(n'[3],n'[2])
    -   * 
    -   * if B < 0 then B = 360 + B
    -   * 
    -   * 
    -   * 
    -   * @param lon
    -   * @param normal
    -   * @return tilt direction in degrees
    -   */
    -  public static double basicTiltDirDeg(double lon, Vector3D normal) {
    +    /**
    +     * Basic tilt direction definition: Project plate normal into the plate facet. Determine angle in
    +     * CW of projected facet assuming up or N is 0deg. Take longitude and generate a quaternion
    +     * representing rotation about the Z-axis. Rotate normal vector by quaternion such that new
    +     * coordinate system x' is now along R. Call this n'.
    +     *
    +     * 
    +     * Tilt direction B = 90 - atan2(n'[3],n'[2])
    +     *
    +     * if B < 0 then B = 360 + B
    +     *
    +     * 
    +     *
    +     * @param lon
    +     * @param normal
    +     * @return tilt direction in degrees
    +     */
    +    public static double basicTiltDirDeg(double lon, Vector3D normal) {
     
    -    Rotation r = new Rotation(Vector3D.PLUS_K, lon, RotationConvention.FRAME_TRANSFORM);
    -    Vector3D rotated = r.applyTo(normal.normalize());
    -    double atan2D = FastMath.atan2(rotated.getZ(), rotated.getY());
    -    double tiltDir = 90 - Math.toDegrees(atan2D);
    -    if (tiltDir < 0) {
    -      tiltDir = 360D + tiltDir;
    +        Rotation r = new Rotation(Vector3D.PLUS_K, lon, RotationConvention.FRAME_TRANSFORM);
    +        Vector3D rotated = r.applyTo(normal.normalize());
    +        double atan2D = FastMath.atan2(rotated.getZ(), rotated.getY());
    +        double tiltDir = 90 - Math.toDegrees(atan2D);
    +        if (tiltDir < 0) {
    +            tiltDir = 360D + tiltDir;
    +        }
    +        return tiltDir;
         }
    -    return tiltDir;
    -
    -  }
    -
    -
     }
    diff --git a/src/main/java/terrasaur/utils/VectorStatistics.java b/src/main/java/terrasaur/utils/VectorStatistics.java
    index 8b97528..fd0c554 100644
    --- a/src/main/java/terrasaur/utils/VectorStatistics.java
    +++ b/src/main/java/terrasaur/utils/VectorStatistics.java
    @@ -31,127 +31,125 @@ import spice.basic.Vector3;
     /**
      * Class which stores a {@link DescriptiveStatistics} object for each dimension in a collection of
      * {@link Vector3D} objects
    - * 
    + *
      * @author Hari.Nair@jhuapl.edu
      *
      */
     public class VectorStatistics {
     
    -  private final DescriptiveStatistics xStats;
    -  private final DescriptiveStatistics yStats;
    -  private final DescriptiveStatistics zStats;
    +    private final DescriptiveStatistics xStats;
    +    private final DescriptiveStatistics yStats;
    +    private final DescriptiveStatistics zStats;
     
    -  private boolean printMedian;
    +    private boolean printMedian;
     
    -  /**
    -   * Toggle printing median statistics in {@link #toString()}. Default is true. Setting to false can
    -   * save some time.
    -   */
    -  public void setPrintMedian(boolean printMedian) {
    -    this.printMedian = printMedian;
    -  }
    -
    -  public VectorStatistics() {
    -    xStats = new DescriptiveStatistics();
    -    yStats = new DescriptiveStatistics();
    -    zStats = new DescriptiveStatistics();
    -    printMedian = true;
    -  }
    -
    -  /**
    -   * Add a vector to the statistics
    -   * 
    -   * @param v vector to add
    -   */
    -  public void add(Vector3D v) {
    -    xStats.addValue(v.getX());
    -    yStats.addValue(v.getY());
    -    zStats.addValue(v.getZ());
    -  }
    -
    -  /**
    -   * Add a vector to the statistics
    -   *
    -   * @param v vector to add
    -   */
    -  public void add(UnwritableVectorIJK v) {
    -    xStats.addValue(v.getI());
    -    yStats.addValue(v.getJ());
    -    zStats.addValue(v.getK());
    -  }
    -
    -  /**
    -   * Add a vector to the statistics
    -   *
    -   * @param v vector to add
    -   */
    -  public void add(Vector3 v) {
    -    double[] values = v.toArray();
    -    xStats.addValue(values[0]);
    -    yStats.addValue(values[1]);
    -    zStats.addValue(values[2]);
    -  }
    -
    -  /**
    -   * @return a {@link Vector3D} where the X component is the mean of the X components, Y is the mean
    -   *         of the Y components, and Z is the mean of the Z components.
    -   */
    -  public Vector3D getMean() {
    -    return new Vector3D(xStats.getMean(), yStats.getMean(), zStats.getMean());
    -  }
    -
    -  /**
    -   * @return a {@link Vector3D} where the X component is the min of the X components, Y is the min
    -   *         of the Y components, and Z is the min of the Z components.
    -   */
    -  public Vector3D getMin() {
    -    return new Vector3D(xStats.getMin(), yStats.getMin(), zStats.getMin());
    -  }
    -
    -  /**
    -   * @return a {@link Vector3D} where the X component is the max of the X components, Y is the max
    -   *         of the Y components, and Z is the max of the Z components.
    -   */
    -  public Vector3D getMax() {
    -    return new Vector3D(xStats.getMax(), yStats.getMax(), zStats.getMax());
    -  }
    -
    -  /**
    -   * @return a {@link Vector3D} where the X component is the std of the X components, Y is the std
    -   *         of the Y components, and Z is the std of the Z components.
    -   */
    -  public Vector3D getStandardDeviation() {
    -    return new Vector3D(xStats.getStandardDeviation(), yStats.getStandardDeviation(),
    -        zStats.getStandardDeviation());
    -  }
    -
    -  @Override
    -  public String toString() {
    -    StringBuilder outBuffer = new StringBuilder();
    -    outBuffer.append("VectorStatistics:\n");
    -    outBuffer.append(String.format("n: [%d,%d,%d]\n", xStats.getN(), yStats.getN(), zStats.getN()));
    -    outBuffer.append(
    -        String.format("min: [%f,%f,%f]\n", xStats.getMin(), yStats.getMin(), zStats.getMin()));
    -    outBuffer.append(
    -        String.format("max: [%f,%f,%f]\n", xStats.getMax(), yStats.getMax(), zStats.getMax()));
    -    outBuffer.append(
    -        String.format("mean: [%f,%f,%f]\n", xStats.getMean(), yStats.getMean(), zStats.getMean()));
    -    outBuffer.append(String.format("std dev: [%f,%f,%f]\n", xStats.getStandardDeviation(),
    -        yStats.getStandardDeviation(), zStats.getStandardDeviation()));
    -    if (printMedian) {
    -      try {
    -        // No catch for MIAE because actual parameter is valid below
    -        outBuffer.append(String.format("median: [%f,%f,%f]\n", xStats.getPercentile(50),
    -            yStats.getPercentile(50), zStats.getPercentile(50)));
    -      } catch (MathIllegalStateException ex) {
    -        outBuffer.append("median: unavailable\n");
    -      }
    +    /**
    +     * Toggle printing median statistics in {@link #toString()}. Default is true. Setting to false can
    +     * save some time.
    +     */
    +    public void setPrintMedian(boolean printMedian) {
    +        this.printMedian = printMedian;
         }
    -    outBuffer.append(String.format("skewness: [%f,%f,%f]\n", xStats.getSkewness(),
    -        yStats.getSkewness(), zStats.getSkewness()));
    -    outBuffer.append(String.format("kurtosis: [%f,%f,%f]\n", xStats.getKurtosis(),
    -        yStats.getKurtosis(), zStats.getKurtosis()));
    -    return outBuffer.toString();
    -  }
     
    +    public VectorStatistics() {
    +        xStats = new DescriptiveStatistics();
    +        yStats = new DescriptiveStatistics();
    +        zStats = new DescriptiveStatistics();
    +        printMedian = true;
    +    }
    +
    +    /**
    +     * Add a vector to the statistics
    +     *
    +     * @param v vector to add
    +     */
    +    public void add(Vector3D v) {
    +        xStats.addValue(v.getX());
    +        yStats.addValue(v.getY());
    +        zStats.addValue(v.getZ());
    +    }
    +
    +    /**
    +     * Add a vector to the statistics
    +     *
    +     * @param v vector to add
    +     */
    +    public void add(UnwritableVectorIJK v) {
    +        xStats.addValue(v.getI());
    +        yStats.addValue(v.getJ());
    +        zStats.addValue(v.getK());
    +    }
    +
    +    /**
    +     * Add a vector to the statistics
    +     *
    +     * @param v vector to add
    +     */
    +    public void add(Vector3 v) {
    +        double[] values = v.toArray();
    +        xStats.addValue(values[0]);
    +        yStats.addValue(values[1]);
    +        zStats.addValue(values[2]);
    +    }
    +
    +    /**
    +     * @return a {@link Vector3D} where the X component is the mean of the X components, Y is the mean
    +     *         of the Y components, and Z is the mean of the Z components.
    +     */
    +    public Vector3D getMean() {
    +        return new Vector3D(xStats.getMean(), yStats.getMean(), zStats.getMean());
    +    }
    +
    +    /**
    +     * @return a {@link Vector3D} where the X component is the min of the X components, Y is the min
    +     *         of the Y components, and Z is the min of the Z components.
    +     */
    +    public Vector3D getMin() {
    +        return new Vector3D(xStats.getMin(), yStats.getMin(), zStats.getMin());
    +    }
    +
    +    /**
    +     * @return a {@link Vector3D} where the X component is the max of the X components, Y is the max
    +     *         of the Y components, and Z is the max of the Z components.
    +     */
    +    public Vector3D getMax() {
    +        return new Vector3D(xStats.getMax(), yStats.getMax(), zStats.getMax());
    +    }
    +
    +    /**
    +     * @return a {@link Vector3D} where the X component is the std of the X components, Y is the std
    +     *         of the Y components, and Z is the std of the Z components.
    +     */
    +    public Vector3D getStandardDeviation() {
    +        return new Vector3D(
    +                xStats.getStandardDeviation(), yStats.getStandardDeviation(), zStats.getStandardDeviation());
    +    }
    +
    +    @Override
    +    public String toString() {
    +        StringBuilder outBuffer = new StringBuilder();
    +        outBuffer.append("VectorStatistics:\n");
    +        outBuffer.append(String.format("n: [%d,%d,%d]\n", xStats.getN(), yStats.getN(), zStats.getN()));
    +        outBuffer.append(String.format("min: [%f,%f,%f]\n", xStats.getMin(), yStats.getMin(), zStats.getMin()));
    +        outBuffer.append(String.format("max: [%f,%f,%f]\n", xStats.getMax(), yStats.getMax(), zStats.getMax()));
    +        outBuffer.append(String.format("mean: [%f,%f,%f]\n", xStats.getMean(), yStats.getMean(), zStats.getMean()));
    +        outBuffer.append(String.format(
    +                "std dev: [%f,%f,%f]\n",
    +                xStats.getStandardDeviation(), yStats.getStandardDeviation(), zStats.getStandardDeviation()));
    +        if (printMedian) {
    +            try {
    +                // No catch for MIAE because actual parameter is valid below
    +                outBuffer.append(String.format(
    +                        "median: [%f,%f,%f]\n",
    +                        xStats.getPercentile(50), yStats.getPercentile(50), zStats.getPercentile(50)));
    +            } catch (MathIllegalStateException ex) {
    +                outBuffer.append("median: unavailable\n");
    +            }
    +        }
    +        outBuffer.append(String.format(
    +                "skewness: [%f,%f,%f]\n", xStats.getSkewness(), yStats.getSkewness(), zStats.getSkewness()));
    +        outBuffer.append(String.format(
    +                "kurtosis: [%f,%f,%f]\n", xStats.getKurtosis(), yStats.getKurtosis(), zStats.getKurtosis()));
    +        return outBuffer.toString();
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/VectorUtils.java b/src/main/java/terrasaur/utils/VectorUtils.java
    index 61e1ebe..b0da66a 100644
    --- a/src/main/java/terrasaur/utils/VectorUtils.java
    +++ b/src/main/java/terrasaur/utils/VectorUtils.java
    @@ -23,33 +23,31 @@
     package terrasaur.utils;
     
     import java.util.Random;
    -import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
     import net.jafama.FastMath;
    +import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
     
     public class VectorUtils {
     
    -  /**
    -   * Return a random unit vector.
    -   * 
    -   * @return
    -   */
    -  public static Vector3D randomVector() {
    -    Random r = new Random();
    -    double delta = FastMath.asin(2 * r.nextDouble() - 1.0);
    -    double alpha = FastMath.PI * (2 * r.nextDouble() - 1.0);
    -    return new Vector3D(alpha, delta);
    -  }
    -
    -  /**
    -   * @param args three floating point numbers separated by commas.
    -   * @return
    -   */
    -  public static Vector3D stringToVector3D(String args) {
    -    String[] params = args.split(",");
    -    double[] array = new double[3];
    -    for (int i = 0; i < 3; i++)
    -      array[i] = Double.valueOf(params[i].trim()).doubleValue();
    -    return new Vector3D(array);
    -  }
    +    /**
    +     * Return a random unit vector.
    +     *
    +     * @return
    +     */
    +    public static Vector3D randomVector() {
    +        Random r = new Random();
    +        double delta = FastMath.asin(2 * r.nextDouble() - 1.0);
    +        double alpha = FastMath.PI * (2 * r.nextDouble() - 1.0);
    +        return new Vector3D(alpha, delta);
    +    }
     
    +    /**
    +     * @param args three floating point numbers separated by commas.
    +     * @return
    +     */
    +    public static Vector3D stringToVector3D(String args) {
    +        String[] params = args.split(",");
    +        double[] array = new double[3];
    +        for (int i = 0; i < 3; i++) array[i] = Double.valueOf(params[i].trim()).doubleValue();
    +        return new Vector3D(array);
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/XYGrid.java b/src/main/java/terrasaur/utils/XYGrid.java
    index 18441af..a4eb13c 100644
    --- a/src/main/java/terrasaur/utils/XYGrid.java
    +++ b/src/main/java/terrasaur/utils/XYGrid.java
    @@ -29,234 +29,223 @@ import java.io.IOException;
     import java.io.OutputStream;
     import java.io.PrintStream;
     import java.util.ArrayList;
    -import terrasaur.smallBodyModel.SmallBodyModel;
     import spice.basic.Plane;
     import spice.basic.SpiceException;
     import spice.basic.Vector3;
    +import terrasaur.smallBodyModel.SmallBodyModel;
     import vtk.vtkPolyData;
     
     /**
      * Class representing a uniformly spaced grid in two dimensions.
    - * 
    + *
      * @author Hari.Nair@jhuapl.edu
      *
      */
     public class XYGrid {
     
    -  class XYGridPoint {
    -    private final int i;
    -    private final int j;
    -    private final double x, y, height;
    -    private final Vector3 pointOnPlane;
    +    class XYGridPoint {
    +        private final int i;
    +        private final int j;
    +        private final double x, y, height;
    +        private final Vector3 pointOnPlane;
     
    -    public XYGridPoint(int i, int j, double x, double y, Vector3 pointOnPlane, double height) {
    -      this.i = i;
    -      this.j = j;
    -      this.x = x;
    -      this.y = y;
    -      this.pointOnPlane = pointOnPlane;
    -      this.height = height;
    +        public XYGridPoint(int i, int j, double x, double y, Vector3 pointOnPlane, double height) {
    +            this.i = i;
    +            this.j = j;
    +            this.x = x;
    +            this.y = y;
    +            this.pointOnPlane = pointOnPlane;
    +            this.height = height;
    +        }
    +
    +        public double getHeight() {
    +            return height;
    +        }
    +
    +        public Vector3 getPointOnPlane() {
    +            return pointOnPlane;
    +        }
         }
     
    -    public double getHeight() {
    -      return height;
    +    private ArrayList gridPoints;
    +
    +    private final Plane plane;
    +    private final Vector3 xAxis;
    +    private final Vector3 yAxis;
    +
    +    private final double resolution;
    +
    +    private final int nx, ny;
    +
    +    public int getNx() {
    +        return nx;
         }
     
    -    public Vector3 getPointOnPlane() {
    -      return pointOnPlane;
    +    public int getNy() {
    +        return ny;
         }
     
    -  }
    +    private final SmallBodyModel smallBodyModel;
     
    -  private ArrayList gridPoints;
    +    private double xmin, xmax, ymin, ymax;
     
    -  private final Plane plane;
    -  private final Vector3 xAxis;
    -  private final Vector3 yAxis;
    +    /**
    +     *
    +     * @param plane reference plane. The X and Y axes are defined by
    +     *        {@link Plane#getSpanningVectors()}.
    +     * @param resolution (in same units as polyData, usually km)
    +     * @param radius half extent of grid: number of grid points in each dimension = (2 * ((int)
    +     *        (radius / resolution)) + 1)
    +     * @param polyData shape model
    +     * @throws SpiceException
    +     */
    +    public XYGrid(Plane plane, double resolution, double radius, vtkPolyData polyData) throws SpiceException {
    +        this.plane = plane;
    +        Vector3[] spanningVectors = plane.getSpanningVectors();
    +        this.xAxis = spanningVectors[0];
    +        this.yAxis = spanningVectors[1];
    +        this.resolution = resolution;
    +        this.smallBodyModel = new SmallBodyModel(polyData);
     
    -  private final double resolution;
    -
    -  private final int nx, ny;
    -
    -  public int getNx() {
    -    return nx;
    -  }
    -
    -  public int getNy() {
    -    return ny;
    -  }
    -
    -  private final SmallBodyModel smallBodyModel;
    -
    -  private double xmin, xmax, ymin, ymax;
    -
    -  /**
    -   * 
    -   * @param plane reference plane. The X and Y axes are defined by
    -   *        {@link Plane#getSpanningVectors()}.
    -   * @param resolution (in same units as polyData, usually km)
    -   * @param radius half extent of grid: number of grid points in each dimension = (2 * ((int)
    -   *        (radius / resolution)) + 1)
    -   * @param polyData shape model
    -   * @throws SpiceException
    -   */
    -  public XYGrid(Plane plane, double resolution, double radius, vtkPolyData polyData)
    -      throws SpiceException {
    -    this.plane = plane;
    -    Vector3[] spanningVectors = plane.getSpanningVectors();
    -    this.xAxis = spanningVectors[0];
    -    this.yAxis = spanningVectors[1];
    -    this.resolution = resolution;
    -    this.smallBodyModel = new SmallBodyModel(polyData);
    -
    -    nx = 2 * ((int) (radius / resolution)) + 1;
    -    ny = 2 * ((int) (radius / resolution)) + 1;
    -  }
    -
    -  /**
    -   * Return a vector shifted by an integral number of grid points in X and Y.
    -   * 
    -   * @param inVector initial point
    -   * @param xShift number of grid points to move in X
    -   * @param yShift number of grid points to move in Y
    -   * @return
    -   */
    -  public Vector3 shift(Vector3 inVector, int xShift, int yShift) {
    -    double x = xShift * resolution;
    -    double y = yShift * resolution;
    -
    -    Vector3 outVector = inVector.add(xAxis.scale(x).add(yAxis.scale(y)));
    -    return outVector;
    -  }
    -
    -  // private vtkPolyData rayBundlePolyData;
    -  // private vtkCellArray rayBundleCells;
    -  // private vtkPoints rayBundlePoints;
    -  // private vtkDoubleArray rayBundleSuccessArray;
    -
    -  // public void writeVTKDebugFile(String filename)
    -  // {
    -  // vtkPolyDataWriter writer=new vtkPolyDataWriter();
    -  // writer.SetInputData(rayBundlePolyData);
    -  // writer.SetFileName(filename);
    -  // writer.SetFileTypeToBinary();
    -  // writer.Update();
    -  // }
    -
    -  /**
    -   * 
    -   * @return heights[ny][nx]
    -   */
    -  public double[][] getHeightGrid() {
    -    double[][] heights = new double[ny][nx];
    -    for (XYGridPoint gridPoint : gridPoints)
    -      heights[gridPoint.i][gridPoint.j] = gridPoint.height;
    -    return heights;
    -  }
    -
    -
    -  /**
    -   * Write out grid coordinates and height.
    -   * 
    -   * @param file File to write
    -   */
    -  public void writeXYZTable(File file) {
    -    try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) {
    -      PrintStream printStream = new PrintStream(outputStream);
    -      for (XYGridPoint gridPoint : gridPoints) {
    -        if (Double.isNaN(gridPoint.height))
    -          continue;
    -
    -        // write values in meters
    -        printStream.printf("%16.8e %16.8e %16.8e\n", 1e3 * gridPoint.x, 1e3 * gridPoint.y,
    -            1e3 * gridPoint.height);
    -      }
    -    } catch (IOException e) {
    -      e.printStackTrace();
    +        nx = 2 * ((int) (radius / resolution)) + 1;
    +        ny = 2 * ((int) (radius / resolution)) + 1;
         }
    -  }
     
    -  /**
    -   * Build a 2D array of heights above a reference plane
    -   * 
    -   * @param point Center of the array
    -   * @throws SpiceException
    -   */
    -  public void buildHeightGrid(Vector3 point) throws SpiceException {
    -    // for debugging
    -    // rayBundlePolyData=new vtkPolyData();
    -    // rayBundleCells=new vtkCellArray();
    -    // rayBundlePoints=new vtkPoints();
    -    // rayBundleSuccessArray=new vtkDoubleArray();
    -    // rayBundleSuccessArray.SetName("success");
    -    //
    -    // rayBundlePolyData.SetPoints(rayBundlePoints);
    -    // rayBundlePolyData.SetLines(rayBundleCells);
    -    // rayBundlePolyData.GetCellData().AddArray(rayBundleSuccessArray);
    -
    -    Vector3 featurePosition = plane.project(point);
    -
    -    gridPoints = new ArrayList();
    -
    -    double[] intersectPoint = new double[3];
    -    double[] origin3D = {0., 0., 0.};
    -    Vector3 pointOnShape = new Vector3();
    -    for (int j = 0; j < ny; j++) {
    -      int yShift = j - ny / 2; // ny is always odd
    -      for (int i = 0; i < nx; i++) {
    -        int xShift = i - nx / 2; // nx is always odd
    +    /**
    +     * Return a vector shifted by an integral number of grid points in X and Y.
    +     *
    +     * @param inVector initial point
    +     * @param xShift number of grid points to move in X
    +     * @param yShift number of grid points to move in Y
    +     * @return
    +     */
    +    public Vector3 shift(Vector3 inVector, int xShift, int yShift) {
             double x = xShift * resolution;
             double y = yShift * resolution;
     
    -        Vector3 pointOnPlane = featurePosition.add(xAxis.scale(x).add(yAxis.scale(y)));
    -        double[] direction = pointOnPlane.hat().toArray();
    -        long cellID = smallBodyModel.computeRayIntersection(origin3D, direction, pointOnPlane.norm(),
    -            intersectPoint);
    +        Vector3 outVector = inVector.add(xAxis.scale(x).add(yAxis.scale(y)));
    +        return outVector;
    +    }
     
    -        // Ray ray = new Ray(new Vector3(origin3D), pointOnPlane.hat());
    -        // double success = 0;
    -        double height = Double.NaN;
    +    // private vtkPolyData rayBundlePolyData;
    +    // private vtkCellArray rayBundleCells;
    +    // private vtkPoints rayBundlePoints;
    +    // private vtkDoubleArray rayBundleSuccessArray;
     
    -        // vtkLine line=new vtkLine();
    -        // int id0=rayBundlePoints.InsertNextPoint(ray.getVertex().toArray());
    -        // int id1=rayBundlePoints.InsertNextPoint(pointOnPlane.add(ray.getVertex()).toArray());
    +    // public void writeVTKDebugFile(String filename)
    +    // {
    +    // vtkPolyDataWriter writer=new vtkPolyDataWriter();
    +    // writer.SetInputData(rayBundlePolyData);
    +    // writer.SetFileName(filename);
    +    // writer.SetFileTypeToBinary();
    +    // writer.Update();
    +    // }
     
    -        if (cellID > 0) {
    -          pointOnShape.assign(intersectPoint);
    -          Vector3 projectedPoint = plane.project(pointOnShape);
    -          height = pointOnShape.sub(projectedPoint).norm();
    -          if (pointOnShape.norm() < projectedPoint.norm())
    -            height *= -1;
    -          // success = 1.;
    -          // id1=rayBundlePoints.InsertNextPoint(pointOnShape.add(ray.getVertex()).toArray());
    +    /**
    +     *
    +     * @return heights[ny][nx]
    +     */
    +    public double[][] getHeightGrid() {
    +        double[][] heights = new double[ny][nx];
    +        for (XYGridPoint gridPoint : gridPoints) heights[gridPoint.i][gridPoint.j] = gridPoint.height;
    +        return heights;
    +    }
    +
    +    /**
    +     * Write out grid coordinates and height.
    +     *
    +     * @param file File to write
    +     */
    +    public void writeXYZTable(File file) {
    +        try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) {
    +            PrintStream printStream = new PrintStream(outputStream);
    +            for (XYGridPoint gridPoint : gridPoints) {
    +                if (Double.isNaN(gridPoint.height)) continue;
    +
    +                // write values in meters
    +                printStream.printf(
    +                        "%16.8e %16.8e %16.8e\n", 1e3 * gridPoint.x, 1e3 * gridPoint.y, 1e3 * gridPoint.height);
    +            }
    +        } catch (IOException e) {
    +            e.printStackTrace();
    +        }
    +    }
    +
    +    /**
    +     * Build a 2D array of heights above a reference plane
    +     *
    +     * @param point Center of the array
    +     * @throws SpiceException
    +     */
    +    public void buildHeightGrid(Vector3 point) throws SpiceException {
    +        // for debugging
    +        // rayBundlePolyData=new vtkPolyData();
    +        // rayBundleCells=new vtkCellArray();
    +        // rayBundlePoints=new vtkPoints();
    +        // rayBundleSuccessArray=new vtkDoubleArray();
    +        // rayBundleSuccessArray.SetName("success");
    +        //
    +        // rayBundlePolyData.SetPoints(rayBundlePoints);
    +        // rayBundlePolyData.SetLines(rayBundleCells);
    +        // rayBundlePolyData.GetCellData().AddArray(rayBundleSuccessArray);
    +
    +        Vector3 featurePosition = plane.project(point);
    +
    +        gridPoints = new ArrayList();
    +
    +        double[] intersectPoint = new double[3];
    +        double[] origin3D = {0., 0., 0.};
    +        Vector3 pointOnShape = new Vector3();
    +        for (int j = 0; j < ny; j++) {
    +            int yShift = j - ny / 2; // ny is always odd
    +            for (int i = 0; i < nx; i++) {
    +                int xShift = i - nx / 2; // nx is always odd
    +                double x = xShift * resolution;
    +                double y = yShift * resolution;
    +
    +                Vector3 pointOnPlane = featurePosition.add(xAxis.scale(x).add(yAxis.scale(y)));
    +                double[] direction = pointOnPlane.hat().toArray();
    +                long cellID =
    +                        smallBodyModel.computeRayIntersection(origin3D, direction, pointOnPlane.norm(), intersectPoint);
    +
    +                // Ray ray = new Ray(new Vector3(origin3D), pointOnPlane.hat());
    +                // double success = 0;
    +                double height = Double.NaN;
    +
    +                // vtkLine line=new vtkLine();
    +                // int id0=rayBundlePoints.InsertNextPoint(ray.getVertex().toArray());
    +                // int id1=rayBundlePoints.InsertNextPoint(pointOnPlane.add(ray.getVertex()).toArray());
    +
    +                if (cellID > 0) {
    +                    pointOnShape.assign(intersectPoint);
    +                    Vector3 projectedPoint = plane.project(pointOnShape);
    +                    height = pointOnShape.sub(projectedPoint).norm();
    +                    if (pointOnShape.norm() < projectedPoint.norm()) height *= -1;
    +                    // success = 1.;
    +                    // id1=rayBundlePoints.InsertNextPoint(pointOnShape.add(ray.getVertex()).toArray());
    +                }
    +
    +                // line.GetPointIds().SetId(0, id0);
    +                // line.GetPointIds().SetId(1, id1);
    +                //
    +                // rayBundleCells.InsertNextCell(line);
    +                // rayBundleSuccessArray.InsertNextValue(success);
    +
    +                gridPoints.add(new XYGridPoint(i, j, x, y, pointOnPlane, height));
    +            }
             }
     
    -        // line.GetPointIds().SetId(0, id0);
    -        // line.GetPointIds().SetId(1, id1);
    -        //
    -        // rayBundleCells.InsertNextCell(line);
    -        // rayBundleSuccessArray.InsertNextValue(success);
    +        xmin = Double.MAX_VALUE;
    +        xmax = -Double.MAX_VALUE;
    +        ymin = Double.MAX_VALUE;
    +        ymax = -Double.MAX_VALUE;
    +        for (XYGridPoint gridPoint : gridPoints) {
    +            if (Double.isNaN(gridPoint.height)) continue;
     
    -        gridPoints.add(new XYGridPoint(i, j, x, y, pointOnPlane, height));
    -      }
    +            if (xmin > gridPoint.x) xmin = gridPoint.x;
    +            if (xmax < gridPoint.x) xmax = gridPoint.x;
    +            if (ymin > gridPoint.y) ymin = gridPoint.y;
    +            if (ymax < gridPoint.y) ymax = gridPoint.y;
    +        }
         }
    -
    -    xmin = Double.MAX_VALUE;
    -    xmax = -Double.MAX_VALUE;
    -    ymin = Double.MAX_VALUE;
    -    ymax = -Double.MAX_VALUE;
    -    for (XYGridPoint gridPoint : gridPoints) {
    -      if (Double.isNaN(gridPoint.height))
    -        continue;
    -
    -      if (xmin > gridPoint.x)
    -        xmin = gridPoint.x;
    -      if (xmax < gridPoint.x)
    -        xmax = gridPoint.x;
    -      if (ymin > gridPoint.y)
    -        ymin = gridPoint.y;
    -      if (ymax < gridPoint.y)
    -        ymax = gridPoint.y;
    -    }
    -  }
     }
    diff --git a/src/main/java/terrasaur/utils/batch/BatchSubmitFactory.java b/src/main/java/terrasaur/utils/batch/BatchSubmitFactory.java
    index 7d7964f..9a2ff7c 100644
    --- a/src/main/java/terrasaur/utils/batch/BatchSubmitFactory.java
    +++ b/src/main/java/terrasaur/utils/batch/BatchSubmitFactory.java
    @@ -28,36 +28,34 @@ import org.apache.logging.log4j.Logger;
     
     /**
      * Factory class for returning concrete classes that implement BatchSubmit interface.
    - * 
    + *
      * @author espirrc1
      *
      */
     public class BatchSubmitFactory {
    -  
    -  private static Logger logger = LogManager.getLogger(BatchSubmitFactory.class);
     
    -  public static BatchSubmitI getBatchSubmit(List commandList, BatchType batchType,
    -      GridType gridType) {
    +    private static Logger logger = LogManager.getLogger(BatchSubmitFactory.class);
     
    -    // check whether or not gridType is valid for the run-time system.
    -    if (!gridType.checkType()) {
    -      logger.info("Changing batch submit to run in local mode!");
    -      gridType = GridType.LOCAL;
    +    public static BatchSubmitI getBatchSubmit(List commandList, BatchType batchType, GridType gridType) {
    +
    +        // check whether or not gridType is valid for the run-time system.
    +        if (!gridType.checkType()) {
    +            logger.info("Changing batch submit to run in local mode!");
    +            gridType = GridType.LOCAL;
    +        }
    +        switch (gridType) {
    +            case SUNOPENGRID:
    +                logger.info("Will run " + gridType + " grid engine.");
    +                return new BatchSubmitOpenGrid(commandList, batchType);
    +
    +            case LOCAL:
    +                logger.info("Will run in local sequential mode!");
    +                return new BatchSubmitLocal(commandList, BatchType.LOCAL_SEQUENTIAL);
    +
    +            // default to returning class that runs in local mode.
    +            default:
    +                logger.info("Could not parse:" + gridType.toString() + ". Will run in local mode!");
    +                return new BatchSubmitLocal(commandList, batchType);
    +        }
         }
    -    switch (gridType) {
    -      case SUNOPENGRID:
    -        logger.info("Will run " + gridType + " grid engine.");
    -        return new BatchSubmitOpenGrid(commandList, batchType);
    -
    -      case LOCAL:
    -        logger.info("Will run in local sequential mode!");
    -        return new BatchSubmitLocal(commandList, BatchType.LOCAL_SEQUENTIAL);
    -
    -      // default to returning class that runs in local mode.
    -      default:
    -        logger.info("Could not parse:" + gridType.toString() + ". Will run in local mode!");
    -        return new BatchSubmitLocal(commandList, batchType);
    -    }
    -  }
    -
     }
    diff --git a/src/main/java/terrasaur/utils/batch/BatchSubmitI.java b/src/main/java/terrasaur/utils/batch/BatchSubmitI.java
    index 293db35..84346b6 100644
    --- a/src/main/java/terrasaur/utils/batch/BatchSubmitI.java
    +++ b/src/main/java/terrasaur/utils/batch/BatchSubmitI.java
    @@ -34,70 +34,67 @@ import java.util.List;
     
     public interface BatchSubmitI {
     
    -  public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException;
    +    public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException;
     
    -  public boolean runProgramAndWait(String program) throws IOException, InterruptedException;
    +    public boolean runProgramAndWait(String program) throws IOException, InterruptedException;
     
    -  public boolean runProgramAndWait(String program, File workingDirectory)
    -      throws IOException, InterruptedException;
    +    public boolean runProgramAndWait(String program, File workingDirectory) throws IOException, InterruptedException;
     
    -  public void limitCores(int limit);
    +    public void limitCores(int limit);
     
    -  public void setQueue(String queueName);
    +    public void setQueue(String queueName);
     
    -  public String printInfo();
    +    public String printInfo();
     
    -  public void noScreenOutput();
    +    public void noScreenOutput();
     
    -  public static  void saveList(List array, String filename) throws IOException {
    -    FileWriter fstream = new FileWriter(filename);
    -    BufferedWriter out = new BufferedWriter(fstream);
    +    public static  void saveList(List array, String filename) throws IOException {
    +        FileWriter fstream = new FileWriter(filename);
    +        BufferedWriter out = new BufferedWriter(fstream);
     
    -    String nl = System.getProperty("line.separator");
    +        String nl = System.getProperty("line.separator");
     
    -    for (T o : array)
    -      out.write(o.toString() + nl);
    +        for (T o : array) out.write(o.toString() + nl);
     
    -    out.close();
    -  }
    -
    -  /**
    -   * Static method that can also be used when one does not need to specify batchType. It will be run
    -   * on machine that is executing the code. User takes responsibility for errors if the program
    -   * itself is expecting a distributed process to exist.
    -   * 
    -   * @param program
    -   * @param workingDirectory
    -   * @return STDOUT created by the program
    -   * @throws IOException
    -   * @throws InterruptedException
    -   */
    -  public static List runAndReturn(String program, File workingDirectory)
    -      throws IOException, InterruptedException {
    -
    -    List stdOut = new ArrayList();
    -
    -    ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    -    processBuilder.directory(workingDirectory);
    -    processBuilder.redirectErrorStream(true);
    -    Process process = processBuilder.start();
    -
    -    InputStream is = process.getInputStream();
    -    InputStreamReader isr = new InputStreamReader(is);
    -    BufferedReader br = new BufferedReader(isr);
    -    String line;
    -    while ((line = br.readLine()) != null) {
    -      stdOut.add(line);
    +        out.close();
         }
     
    -    int exitStatus = process.waitFor();
    -    br.close();
    -    process.destroy();
    -    if (exitStatus != 0) {
    -      stdOut.add("Error! non-zero status returned!");
    +    /**
    +     * Static method that can also be used when one does not need to specify batchType. It will be run
    +     * on machine that is executing the code. User takes responsibility for errors if the program
    +     * itself is expecting a distributed process to exist.
    +     *
    +     * @param program
    +     * @param workingDirectory
    +     * @return STDOUT created by the program
    +     * @throws IOException
    +     * @throws InterruptedException
    +     */
    +    public static List runAndReturn(String program, File workingDirectory)
    +            throws IOException, InterruptedException {
    +
    +        List stdOut = new ArrayList();
    +
    +        ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    +        processBuilder.directory(workingDirectory);
    +        processBuilder.redirectErrorStream(true);
    +        Process process = processBuilder.start();
    +
    +        InputStream is = process.getInputStream();
    +        InputStreamReader isr = new InputStreamReader(is);
    +        BufferedReader br = new BufferedReader(isr);
    +        String line;
    +        while ((line = br.readLine()) != null) {
    +            stdOut.add(line);
    +        }
    +
    +        int exitStatus = process.waitFor();
    +        br.close();
    +        process.destroy();
    +        if (exitStatus != 0) {
    +            stdOut.add("Error! non-zero status returned!");
    +        }
    +
    +        return stdOut;
         }
    -
    -    return stdOut;
    -  }
    -
     }
    diff --git a/src/main/java/terrasaur/utils/batch/BatchSubmitLocal.java b/src/main/java/terrasaur/utils/batch/BatchSubmitLocal.java
    index b900fdb..429f6d6 100644
    --- a/src/main/java/terrasaur/utils/batch/BatchSubmitLocal.java
    +++ b/src/main/java/terrasaur/utils/batch/BatchSubmitLocal.java
    @@ -35,190 +35,184 @@ import org.apache.logging.log4j.Logger;
     /**
      * Contains concrete methods for submitting batch jobs to local machine. So even if batchType says
      * "GRID*" will default to local.
    - * 
    + *
      * @author espirrc1
      *
      */
     public class BatchSubmitLocal implements BatchSubmitI {
     
    -  private static Logger logger = LogManager.getLogger(BatchSubmitLocal.class);
    +    private static Logger logger = LogManager.getLogger(BatchSubmitLocal.class);
     
    -  private List commandList;
    -  private BatchType batchType;
    -  private int cores;
    -  private boolean showOutput = true;
    -  // private String gridQueue = null;
    +    private List commandList;
    +    private BatchType batchType;
    +    private int cores;
    +    private boolean showOutput = true;
    +    // private String gridQueue = null;
     
    -  public BatchSubmitLocal(List commandList, BatchType batchType) {
    -    this.commandList = commandList;
    -    this.batchType = batchType;
    -    cores = Runtime.getRuntime().availableProcessors();
    -  }
    -
    -  /**
    -   * STDOUT from commandlist will not be printed.
    -   */
    -  public void noScreenOutput() {
    -    showOutput = false;
    -  }
    -
    -  /**
    -   * Does not apply in local mode.
    -   * 
    -   * @param gridQueue
    -   */
    -  public void setQueue(String gridQueue) {
    -    logger.warn(
    -        "Warning: trying to set a queue while running local mode, does" + " not do anything.");
    -  }
    -
    -  public void limitCores(int limit) {
    -    cores = Math.min(limit, Runtime.getRuntime().availableProcessors());
    -    logger.info("limiting number of local cores to:" + cores);
    -  }
    -
    -  public String printInfo() {
    -    return "batchType:" + this.batchType;
    -  }
    -
    -  /**
    -   * Run batch submission in the specified working directory. Uses current working directory if
    -   * workingDir is null.
    -   * 
    -   * @param workingDir
    -   * @return
    -   * @throws InterruptedException
    -   * @throws IOException
    -   */
    -  public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException {
    -
    -    // evaluate workingDir. If empty string then set to null;
    -    workingDir = emptyToNull(workingDir);
    -
    -    switch (batchType) {
    -      case GNU_PARALLEL:
    -        return runBatchSubmitProgramParallel(commandList);
    -
    -      case LOCAL_SEQUENTIAL:
    -        return runBatchSubmitProgramLocalSequential(commandList, workingDir);
    -
    -      case GRID_ENGINE:
    -      case GRID_ENGINE_8:
    -      case GRID_ENGINE_6:
    -      case GRID_ENGINE_4:
    -      case GRID_ENGINE_3:
    -      case GRID_ENGINE_2:
    -      default:
    -        throw new RuntimeException("Can't submit " + batchType.toString() + " in "
    -            + "BatchSubmitLocal class. Please fix pipe config file.");
    -    }
    -  }
    -
    -  public boolean runProgramAndWait(String program) throws IOException, InterruptedException {
    -    return runProgramAndWait(program, null);
    -  }
    -
    -  public boolean runProgramAndWait(String program, File workingDirectory)
    -      throws IOException, InterruptedException {
    -
    -    // return runAndWait(program, workingDirectory, showOutput);
    -    ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    -    processBuilder.directory(workingDirectory);
    -    processBuilder.redirectErrorStream(true);
    -    Process process = processBuilder.start();
    -
    -    InputStream is = process.getInputStream();
    -    InputStreamReader isr = new InputStreamReader(is);
    -    BufferedReader br = new BufferedReader(isr);
    -    String line;
    -    if (showOutput) {
    -      logger.printf(Level.INFO, "Output of running %s is:", program);
    -      while ((line = br.readLine()) != null) {
    -        System.out.println(line);
    -      }
    -    } else {
    -      logger.printf(Level.INFO, "Output of running %s disabled.", program);
    +    public BatchSubmitLocal(List commandList, BatchType batchType) {
    +        this.commandList = commandList;
    +        this.batchType = batchType;
    +        cores = Runtime.getRuntime().availableProcessors();
         }
     
    -    int exitStatus = process.waitFor();
    -    logger.info("Program " + program + " finished with status: " + exitStatus);
    -    br.close();
    -    process.destroy();
    -
    -    if (exitStatus != 0) {
    -      logger.error("Terminating since subprogram failed.");
    -      System.exit(exitStatus);
    +    /**
    +     * STDOUT from commandlist will not be printed.
    +     */
    +    public void noScreenOutput() {
    +        showOutput = false;
         }
     
    -    return exitStatus == 0;
    -  }
    -
    -  /**
    -   * Static method that can also be used when one does not need to specify batchType. It will be run
    -   * on machine that is executing the code. User takes responsibility for errors if the program
    -   * itself is expecting a distributed process to exist.
    -   * 
    -   * @param program
    -   * @param workingDirectory
    -   * @param showOutput
    -   * @return
    -   * @throws IOException
    -   * @throws InterruptedException
    -   */
    -  public static boolean runAndWait(String program, File workingDirectory, boolean showOutput)
    -      throws IOException, InterruptedException {
    -
    -    return false;
    -  }
    -
    -  /**
    -   * Evaluate working directory string. Set to null if empty.
    -   * 
    -   * @param workingDir
    -   * @return
    -   */
    -  private String emptyToNull(String workingDir) {
    -    if (workingDir != null) {
    -      if (workingDir.length() < 1) {
    -        return null;
    -      } else {
    -      }
    -    }
    -    return workingDir;
    -  }
    -
    -  private boolean runBatchSubmitProgramParallel(List commandList)
    -      throws InterruptedException, IOException {
    -    // Create a text file with all the commands that should be run, one per
    -    // line
    -    File temp = File.createTempFile("altwg-batch-list", ".tmp", null);
    -    BatchSubmitI.saveList(commandList, temp.getAbsolutePath());
    -
    -    // Now submit all these batches GNU Parallel
    -    String batchSubmitCommand = "parallel -v -a " + temp.getAbsolutePath();
    -
    -    return runProgramAndWait(batchSubmitCommand);
    -  }
    -
    -  private boolean runBatchSubmitProgramLocalSequential(List commandList, String workingDir)
    -      throws IOException, InterruptedException {
    -
    -    // evaluate workingDir. If empty string then set to null;
    -    workingDir = emptyToNull(workingDir);
    -
    -    boolean successful = true;
    -    for (String command : commandList) {
    -      if (workingDir != null) {
    -        File workingFile = new File(workingDir);
    -        if (!runProgramAndWait(command, workingFile))
    -          successful = false;
    -      } else {
    -        if (!runProgramAndWait(command))
    -          successful = false;
    -      }
    +    /**
    +     * Does not apply in local mode.
    +     *
    +     * @param gridQueue
    +     */
    +    public void setQueue(String gridQueue) {
    +        logger.warn("Warning: trying to set a queue while running local mode, does" + " not do anything.");
         }
     
    -    return successful;
    -  }
    +    public void limitCores(int limit) {
    +        cores = Math.min(limit, Runtime.getRuntime().availableProcessors());
    +        logger.info("limiting number of local cores to:" + cores);
    +    }
     
    +    public String printInfo() {
    +        return "batchType:" + this.batchType;
    +    }
    +
    +    /**
    +     * Run batch submission in the specified working directory. Uses current working directory if
    +     * workingDir is null.
    +     *
    +     * @param workingDir
    +     * @return
    +     * @throws InterruptedException
    +     * @throws IOException
    +     */
    +    public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException {
    +
    +        // evaluate workingDir. If empty string then set to null;
    +        workingDir = emptyToNull(workingDir);
    +
    +        switch (batchType) {
    +            case GNU_PARALLEL:
    +                return runBatchSubmitProgramParallel(commandList);
    +
    +            case LOCAL_SEQUENTIAL:
    +                return runBatchSubmitProgramLocalSequential(commandList, workingDir);
    +
    +            case GRID_ENGINE:
    +            case GRID_ENGINE_8:
    +            case GRID_ENGINE_6:
    +            case GRID_ENGINE_4:
    +            case GRID_ENGINE_3:
    +            case GRID_ENGINE_2:
    +            default:
    +                throw new RuntimeException("Can't submit " + batchType.toString() + " in "
    +                        + "BatchSubmitLocal class. Please fix pipe config file.");
    +        }
    +    }
    +
    +    public boolean runProgramAndWait(String program) throws IOException, InterruptedException {
    +        return runProgramAndWait(program, null);
    +    }
    +
    +    public boolean runProgramAndWait(String program, File workingDirectory) throws IOException, InterruptedException {
    +
    +        // return runAndWait(program, workingDirectory, showOutput);
    +        ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    +        processBuilder.directory(workingDirectory);
    +        processBuilder.redirectErrorStream(true);
    +        Process process = processBuilder.start();
    +
    +        InputStream is = process.getInputStream();
    +        InputStreamReader isr = new InputStreamReader(is);
    +        BufferedReader br = new BufferedReader(isr);
    +        String line;
    +        if (showOutput) {
    +            logger.printf(Level.INFO, "Output of running %s is:", program);
    +            while ((line = br.readLine()) != null) {
    +                System.out.println(line);
    +            }
    +        } else {
    +            logger.printf(Level.INFO, "Output of running %s disabled.", program);
    +        }
    +
    +        int exitStatus = process.waitFor();
    +        logger.info("Program " + program + " finished with status: " + exitStatus);
    +        br.close();
    +        process.destroy();
    +
    +        if (exitStatus != 0) {
    +            logger.error("Terminating since subprogram failed.");
    +            System.exit(exitStatus);
    +        }
    +
    +        return exitStatus == 0;
    +    }
    +
    +    /**
    +     * Static method that can also be used when one does not need to specify batchType. It will be run
    +     * on machine that is executing the code. User takes responsibility for errors if the program
    +     * itself is expecting a distributed process to exist.
    +     *
    +     * @param program
    +     * @param workingDirectory
    +     * @param showOutput
    +     * @return
    +     * @throws IOException
    +     * @throws InterruptedException
    +     */
    +    public static boolean runAndWait(String program, File workingDirectory, boolean showOutput)
    +            throws IOException, InterruptedException {
    +
    +        return false;
    +    }
    +
    +    /**
    +     * Evaluate working directory string. Set to null if empty.
    +     *
    +     * @param workingDir
    +     * @return
    +     */
    +    private String emptyToNull(String workingDir) {
    +        if (workingDir != null) {
    +            if (workingDir.length() < 1) {
    +                return null;
    +            } else {
    +            }
    +        }
    +        return workingDir;
    +    }
    +
    +    private boolean runBatchSubmitProgramParallel(List commandList) throws InterruptedException, IOException {
    +        // Create a text file with all the commands that should be run, one per
    +        // line
    +        File temp = File.createTempFile("altwg-batch-list", ".tmp", null);
    +        BatchSubmitI.saveList(commandList, temp.getAbsolutePath());
    +
    +        // Now submit all these batches GNU Parallel
    +        String batchSubmitCommand = "parallel -v -a " + temp.getAbsolutePath();
    +
    +        return runProgramAndWait(batchSubmitCommand);
    +    }
    +
    +    private boolean runBatchSubmitProgramLocalSequential(List commandList, String workingDir)
    +            throws IOException, InterruptedException {
    +
    +        // evaluate workingDir. If empty string then set to null;
    +        workingDir = emptyToNull(workingDir);
    +
    +        boolean successful = true;
    +        for (String command : commandList) {
    +            if (workingDir != null) {
    +                File workingFile = new File(workingDir);
    +                if (!runProgramAndWait(command, workingFile)) successful = false;
    +            } else {
    +                if (!runProgramAndWait(command)) successful = false;
    +            }
    +        }
    +
    +        return successful;
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/batch/BatchSubmitOpenGrid.java b/src/main/java/terrasaur/utils/batch/BatchSubmitOpenGrid.java
    index a2ee0f3..8b45522 100644
    --- a/src/main/java/terrasaur/utils/batch/BatchSubmitOpenGrid.java
    +++ b/src/main/java/terrasaur/utils/batch/BatchSubmitOpenGrid.java
    @@ -37,369 +37,356 @@ import org.apache.logging.log4j.Logger;
     /**
      * Contains concrete methods for submitting batch jobs to Sun Open Grid Grid Computing software in
      * addition to supporting submission of jobs to local sequential or local parallel.
    - * 
    + *
      * This class assumes that the Sun Open Grid Engine does exist and is able to accept jobs, hence no
      * need to check for existence of $SGE_ROOT environment variable.
    - * 
    + *
      * @author espirrc1
      *
      */
     public class BatchSubmitOpenGrid implements BatchSubmitI {
     
    -  private static Logger logger = LogManager.getLogger(BatchSubmitOpenGrid.class);
    +    private static Logger logger = LogManager.getLogger(BatchSubmitOpenGrid.class);
     
    -  private List commandList;
    -  private BatchType batchType;
    -  private int cores;
    -  private boolean showOutput = true;
    -  private String gridQueue = null;
    +    private List commandList;
    +    private BatchType batchType;
    +    private int cores;
    +    private boolean showOutput = true;
    +    private String gridQueue = null;
     
    -  public BatchSubmitOpenGrid(List commandList, BatchType batchType) {
    -    this.commandList = commandList;
    -    this.batchType = batchType;
    -    cores = Runtime.getRuntime().availableProcessors();
    -  }
    -
    -  /**
    -   * STDOUT from commandlist will not be printed.
    -   */
    -  @Override
    -  public void noScreenOutput() {
    -    showOutput = false;
    -  }
    -
    -  /**
    -   * Set the grid queue to use when calling the grid engine.
    -   * 
    -   * @param queueName
    -   */
    -  @Override
    -  public void setQueue(String queueName) {
    -    this.gridQueue = queueName;
    -  }
    -
    -  @Override
    -  public void limitCores(int limit) {
    -    cores = Math.min(limit, Runtime.getRuntime().availableProcessors());
    -    logger.info("limiting number of local cores to:" + cores);
    -  }
    -
    -  @Override
    -  public String printInfo() {
    -    StringBuilder sb = new StringBuilder();
    -    sb.append("batchType:" + this.batchType);
    -    if (gridQueue != null) {
    -      sb.append("queue:" + gridQueue);
    -    }
    -    return sb.toString();
    -  }
    -
    -  /**
    -   * Run batch submission in the specified working directory. Uses current working directory if
    -   * workingDir is null.
    -   * 
    -   * @param workingDir
    -   * @return
    -   * @throws InterruptedException
    -   * @throws IOException
    -   */
    -  @Override
    -  public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException {
    -
    -    // evaluate workingDir. If empty string then set to null;
    -    workingDir = emptyToNull(workingDir);
    -
    -    logger.info("Running:" + batchType.toString());
    -
    -    switch (batchType) {
    -      case GRID_ENGINE:
    -        return runBatchSubmitProgramGridEngine(commandList, workingDir);
    -
    -      case GRID_ENGINE_45:
    -      case GRID_ENGINE_35:
    -      case GRID_ENGINE_33:
    -      case GRID_ENGINE_32:
    -      case GRID_ENGINE_23:
    -      case GRID_ENGINE_17:
    -      case GRID_ENGINE_16:
    -      case GRID_ENGINE_10:
    -      case GRID_ENGINE_8:
    -      case GRID_ENGINE_6:
    -      case GRID_ENGINE_4:
    -      case GRID_ENGINE_3:
    -      case GRID_ENGINE_2:
    -        return runBatchSubmitProgramGridEngineLimitSlots(commandList, workingDir, batchType);
    -
    -      case GNU_PARALLEL:
    -        return runBatchSubmitProgramParallel(commandList);
    -
    -      case LOCAL_SEQUENTIAL:
    -        return runBatchSubmitProgramLocalSequential(commandList, workingDir);
    -
    -      default:
    -        String errMesg =
    -            "ERROR! Batch type:" + batchType.toString() + " not supported by runBatchSubmitinDir!";
    -        throw new RuntimeException(errMesg);
    -
    -    }
    -  }
    -
    -  @Override
    -  public boolean runProgramAndWait(String program) throws IOException, InterruptedException {
    -    return runProgramAndWait(program, null);
    -  }
    -
    -
    -  @Override
    -  public boolean runProgramAndWait(String program, File workingDirectory)
    -      throws IOException, InterruptedException {
    -
    -    // return runAndWait(program, workingDirectory, showOutput);
    -    ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    -    processBuilder.directory(workingDirectory);
    -    processBuilder.redirectErrorStream(true);
    -    Process process = processBuilder.start();
    -
    -    InputStream is = process.getInputStream();
    -    InputStreamReader isr = new InputStreamReader(is);
    -    BufferedReader br = new BufferedReader(isr);
    -    String line;
    -
    -    // disable output for now. See if this
    -    if (showOutput) {
    -      logger.printf(Level.INFO, "Output of running %s is:", program);
    -      while ((line = br.readLine()) != null) {
    -        System.out.println(line);
    -      }
    -      System.out.flush();
    -    } else {
    -      logger.printf(Level.INFO, "Output of running %s disabled.", program);
    -    }
    -    int exitStatus = process.waitFor();
    -    logger.info("Program " + program + " finished with status: " + exitStatus);
    -    br.close();
    -    process.destroy();
    -
    -    if (exitStatus != 0) {
    -      logger.error("Terminating since subprogram failed.");
    -      System.exit(exitStatus);
    -    }
    -    return exitStatus == 0;
    -  }
    -
    -  /**
    -   * Static method that can also be used when one does not need to specify batchType. It will be run
    -   * on machine that is executing the code. User takes responsibility for errors if the program
    -   * itself is expecting a distributed process to exist.
    -   * 
    -   * @param program
    -   * @param workingDirectory
    -   * @param showOutput
    -   * @return
    -   * @throws IOException
    -   * @throws InterruptedException
    -   */
    -  public static boolean runAndWait(String program, File workingDirectory, boolean showOutput)
    -      throws IOException, InterruptedException {
    -
    -    return false;
    -  }
    -
    -  /**
    -   * Evaluate working directory string. Set to null if empty.
    -   * 
    -   * @param workingDir
    -   * @return
    -   */
    -  private String emptyToNull(String workingDir) {
    -    if (workingDir != null) {
    -      if (workingDir.length() < 1) {
    -        return null;
    -      } else {
    -      }
    -    }
    -    return workingDir;
    -  }
    -
    -  private boolean runBatchSubmitProgramGridEngine(List commandList, String workingDir)
    -      throws InterruptedException, IOException {
    -
    -    workingDir = emptyToNull(workingDir);
    -
    -    // Create a text file for input to qsub making use of qsub's job array
    -    // option. Use workingDir if not null
    -    File tempDir = null;
    -    if (workingDir != null) {
    -      tempDir = new File(workingDir);
    -    }
    -    File temp = File.createTempFile("altwg-batch-list", ".bash", tempDir);
    -
    -    FileWriter ofs = new FileWriter(temp);
    -    BufferedWriter out = new BufferedWriter(ofs);
    -    out.write("CURRENTDATE=`date +\"%Y-%m-%d %T\"`\n");
    -    out.write("echo $CURRENTDATE The node running this job is $HOSTNAME\n");
    -    for (int i = 1; i <= commandList.size(); ++i)
    -      out.write("if [ $SGE_TASK_ID -eq " + i + " ]; then \n " + commandList.get(i - 1)
    -          + "\n exit $?\nfi\n");
    -    out.close();
    -
    -    String batchSubmitCommand;
    -
    -    String queue = "";
    -    if ((gridQueue != null) && (gridQueue.length() > 1)) {
    -      // use grid queue name explicitly
    -      queue = " -q " + gridQueue + " ";
    +    public BatchSubmitOpenGrid(List commandList, BatchType batchType) {
    +        this.commandList = commandList;
    +        this.batchType = batchType;
    +        cores = Runtime.getRuntime().availableProcessors();
         }
     
    -    if (workingDir != null) {
    -
    -      // user specified working directory
    -      batchSubmitCommand = "qsub " + queue + "-S /bin/bash -V -wd " + workingDir + " -sync y -t 1-"
    -          + commandList.size() + " " + temp.getAbsolutePath();
    -
    -    } else {
    -      // null working directory. Assume want to work in current working directory
    -      batchSubmitCommand = "qsub " + queue + "-S /bin/bash -V -cwd -sync y -t 1-"
    -          + commandList.size() + " " + temp.getAbsolutePath();
    -
    +    /**
    +     * STDOUT from commandlist will not be printed.
    +     */
    +    @Override
    +    public void noScreenOutput() {
    +        showOutput = false;
         }
     
    -    boolean success = runProgramAndWait(batchSubmitCommand);
    -
    -    if (success) {
    -      // If no error, delete qsub output and error files
    -      File[] filelist = new File(".").listFiles();
    -      for (File f : filelist) {
    -        if (f.getName().startsWith(temp.getName()))
    -          f.delete();
    -      }
    +    /**
    +     * Set the grid queue to use when calling the grid engine.
    +     *
    +     * @param queueName
    +     */
    +    @Override
    +    public void setQueue(String queueName) {
    +        this.gridQueue = queueName;
         }
     
    -    return success;
    -  }
    -
    -  /**
    -   * Call the grid engine using the parallel environment that has been configured for it. The
    -   * batchType enumeration should contain a number specifying the number of CPUs needed per job.
    -   * Useful when trying to run memory intensive programs such as DistributedGravity.java.
    -   * 
    -   * @param commandList
    -   * @return
    -   * @throws InterruptedException
    -   * @throws IOException
    -   */
    -  private boolean runBatchSubmitProgramGridEngineLimitSlots(List commandList,
    -      String workingDir, BatchType batchType) throws InterruptedException, IOException {
    -
    -    // evaluate workingDir. If empty string then set to null;
    -    workingDir = emptyToNull(workingDir);
    -
    -    // Create a text file for input to qsub making use of qsub's job array
    -    // option
    -    File tempDir = null;
    -    if (workingDir != null) {
    -      tempDir = new File(workingDir);
    -    }
    -    File temp = File.createTempFile("altwg-batch-list", ".bash", tempDir);
    -
    -    FileWriter ofs = new FileWriter(temp);
    -    BufferedWriter out = new BufferedWriter(ofs);
    -    out.write("CURRENTDATE=`date +\"%Y-%m-%d %T\"`\n");
    -    out.write("echo $CURRENTDATE The node running this job is $HOSTNAME\n");
    -    for (int i = 1; i <= commandList.size(); ++i)
    -      out.write("if [ $SGE_TASK_ID -eq " + i + " ]; then \n " + commandList.get(i - 1)
    -          + "\n exit $?\nfi\n");
    -    out.close();
    -
    -    // default to using 6 cpus per job.
    -    String cpus = "6";
    -    // String batchString = batchType.toString();
    -    int numSlots = BatchType.slotPerJob(batchType);
    -    if (numSlots == 999) {
    -      String errMesg = "ERROR! batchType:" + batchType.toString() + " not supported by"
    -          + " BatchType.slotsPerJob()!";
    -      throw new RuntimeException(errMesg);
    -    }
    -    cpus = Integer.toString(numSlots);
    -
    -    // check to see if grid queue is specified
    -    StringBuilder batchCmd = new StringBuilder("qsub");
    -    if ((gridQueue != null) && (gridQueue.length() > 1)) {
    -
    -      // use grid queue name explicitly
    -      batchCmd.append(" -q " + gridQueue);
    +    @Override
    +    public void limitCores(int limit) {
    +        cores = Math.min(limit, Runtime.getRuntime().availableProcessors());
    +        logger.info("limiting number of local cores to:" + cores);
         }
     
    -    // Try to use parallel processing environment
    -    batchCmd.append(" -pe ocmp");
    -    batchCmd.append(" " + cpus);
    -
    -    batchCmd.append(" -S /bin/bash");
    -    batchCmd.append(" -V");
    -
    -    if (workingDir != null) {
    -      batchCmd.append(" -wd " + workingDir);
    -    } else {
    -      batchCmd.append(" -cwd ");
    -    }
    -    batchCmd.append(" -sync y");
    -    batchCmd.append(" -t 1-" + commandList.size());
    -    batchCmd.append(" " + temp.getAbsolutePath());
    -
    -    // batchSubmitCommand = "qsub -pe ocmp " + cpus + " -S /bin/bash -V -cwd -sync y -t 1-" +
    -    // commandList.size() + " "
    -    // + temp.getAbsolutePath();
    -    // batchSubmitCommand = "qsub -pe ocmp " + cpus + " -S /bin/bash -V -wd " + workingDir + " -sync
    -    // y -t 1-" + commandList.size() + " "
    -    // + temp.getAbsolutePath();
    -
    -    String batchSubmitCommand = batchCmd.toString();
    -    boolean success = runProgramAndWait(batchSubmitCommand);
    -
    -    if (success) {
    -      // If no error, delete qsub output and error files
    -      File[] filelist = new File(".").listFiles();
    -      for (File f : filelist) {
    -        if (f.getName().startsWith(temp.getName()))
    -          f.delete();
    -      }
    +    @Override
    +    public String printInfo() {
    +        StringBuilder sb = new StringBuilder();
    +        sb.append("batchType:" + this.batchType);
    +        if (gridQueue != null) {
    +            sb.append("queue:" + gridQueue);
    +        }
    +        return sb.toString();
         }
     
    -    return success;
    -  }
    +    /**
    +     * Run batch submission in the specified working directory. Uses current working directory if
    +     * workingDir is null.
    +     *
    +     * @param workingDir
    +     * @return
    +     * @throws InterruptedException
    +     * @throws IOException
    +     */
    +    @Override
    +    public boolean runBatchSubmitinDir(String workingDir) throws InterruptedException, IOException {
     
    -  private boolean runBatchSubmitProgramParallel(List commandList)
    -      throws InterruptedException, IOException {
    -    // Create a text file with all the commands that should be run, one per
    -    // line
    -    File temp = File.createTempFile("altwg-batch-list", ".tmp", null);
    -    BatchSubmitI.saveList(commandList, temp.getAbsolutePath());
    +        // evaluate workingDir. If empty string then set to null;
    +        workingDir = emptyToNull(workingDir);
     
    -    // Now submit all these batches GNU Parallel
    -    String batchSubmitCommand = "parallel -v -a " + temp.getAbsolutePath();
    +        logger.info("Running:" + batchType.toString());
     
    -    return runProgramAndWait(batchSubmitCommand);
    -  }
    +        switch (batchType) {
    +            case GRID_ENGINE:
    +                return runBatchSubmitProgramGridEngine(commandList, workingDir);
     
    -  private boolean runBatchSubmitProgramLocalSequential(List commandList, String workingDir)
    -      throws IOException, InterruptedException {
    +            case GRID_ENGINE_45:
    +            case GRID_ENGINE_35:
    +            case GRID_ENGINE_33:
    +            case GRID_ENGINE_32:
    +            case GRID_ENGINE_23:
    +            case GRID_ENGINE_17:
    +            case GRID_ENGINE_16:
    +            case GRID_ENGINE_10:
    +            case GRID_ENGINE_8:
    +            case GRID_ENGINE_6:
    +            case GRID_ENGINE_4:
    +            case GRID_ENGINE_3:
    +            case GRID_ENGINE_2:
    +                return runBatchSubmitProgramGridEngineLimitSlots(commandList, workingDir, batchType);
     
    -    // evaluate workingDir. If empty string then set to null;
    -    workingDir = emptyToNull(workingDir);
    +            case GNU_PARALLEL:
    +                return runBatchSubmitProgramParallel(commandList);
     
    -    boolean successful = true;
    -    for (String command : commandList) {
    -      if (workingDir != null) {
    -        File workingFile = new File(workingDir);
    -        if (!runProgramAndWait(command, workingFile))
    -          successful = false;
    -      } else {
    -        if (!runProgramAndWait(command))
    -          successful = false;
    -      }
    +            case LOCAL_SEQUENTIAL:
    +                return runBatchSubmitProgramLocalSequential(commandList, workingDir);
    +
    +            default:
    +                String errMesg = "ERROR! Batch type:" + batchType.toString() + " not supported by runBatchSubmitinDir!";
    +                throw new RuntimeException(errMesg);
    +        }
         }
     
    -    return successful;
    -  }
    +    @Override
    +    public boolean runProgramAndWait(String program) throws IOException, InterruptedException {
    +        return runProgramAndWait(program, null);
    +    }
     
    +    @Override
    +    public boolean runProgramAndWait(String program, File workingDirectory) throws IOException, InterruptedException {
    +
    +        // return runAndWait(program, workingDirectory, showOutput);
    +        ProcessBuilder processBuilder = new ProcessBuilder(program.split("\\s+"));
    +        processBuilder.directory(workingDirectory);
    +        processBuilder.redirectErrorStream(true);
    +        Process process = processBuilder.start();
    +
    +        InputStream is = process.getInputStream();
    +        InputStreamReader isr = new InputStreamReader(is);
    +        BufferedReader br = new BufferedReader(isr);
    +        String line;
    +
    +        // disable output for now. See if this
    +        if (showOutput) {
    +            logger.printf(Level.INFO, "Output of running %s is:", program);
    +            while ((line = br.readLine()) != null) {
    +                System.out.println(line);
    +            }
    +            System.out.flush();
    +        } else {
    +            logger.printf(Level.INFO, "Output of running %s disabled.", program);
    +        }
    +        int exitStatus = process.waitFor();
    +        logger.info("Program " + program + " finished with status: " + exitStatus);
    +        br.close();
    +        process.destroy();
    +
    +        if (exitStatus != 0) {
    +            logger.error("Terminating since subprogram failed.");
    +            System.exit(exitStatus);
    +        }
    +        return exitStatus == 0;
    +    }
    +
    +    /**
    +     * Static method that can also be used when one does not need to specify batchType. It will be run
    +     * on machine that is executing the code. User takes responsibility for errors if the program
    +     * itself is expecting a distributed process to exist.
    +     *
    +     * @param program
    +     * @param workingDirectory
    +     * @param showOutput
    +     * @return
    +     * @throws IOException
    +     * @throws InterruptedException
    +     */
    +    public static boolean runAndWait(String program, File workingDirectory, boolean showOutput)
    +            throws IOException, InterruptedException {
    +
    +        return false;
    +    }
    +
    +    /**
    +     * Evaluate working directory string. Set to null if empty.
    +     *
    +     * @param workingDir
    +     * @return
    +     */
    +    private String emptyToNull(String workingDir) {
    +        if (workingDir != null) {
    +            if (workingDir.length() < 1) {
    +                return null;
    +            } else {
    +            }
    +        }
    +        return workingDir;
    +    }
    +
    +    private boolean runBatchSubmitProgramGridEngine(List commandList, String workingDir)
    +            throws InterruptedException, IOException {
    +
    +        workingDir = emptyToNull(workingDir);
    +
    +        // Create a text file for input to qsub making use of qsub's job array
    +        // option. Use workingDir if not null
    +        File tempDir = null;
    +        if (workingDir != null) {
    +            tempDir = new File(workingDir);
    +        }
    +        File temp = File.createTempFile("altwg-batch-list", ".bash", tempDir);
    +
    +        FileWriter ofs = new FileWriter(temp);
    +        BufferedWriter out = new BufferedWriter(ofs);
    +        out.write("CURRENTDATE=`date +\"%Y-%m-%d %T\"`\n");
    +        out.write("echo $CURRENTDATE The node running this job is $HOSTNAME\n");
    +        for (int i = 1; i <= commandList.size(); ++i)
    +            out.write("if [ $SGE_TASK_ID -eq " + i + " ]; then \n " + commandList.get(i - 1) + "\n exit $?\nfi\n");
    +        out.close();
    +
    +        String batchSubmitCommand;
    +
    +        String queue = "";
    +        if ((gridQueue != null) && (gridQueue.length() > 1)) {
    +            // use grid queue name explicitly
    +            queue = " -q " + gridQueue + " ";
    +        }
    +
    +        if (workingDir != null) {
    +
    +            // user specified working directory
    +            batchSubmitCommand = "qsub " + queue + "-S /bin/bash -V -wd " + workingDir + " -sync y -t 1-"
    +                    + commandList.size() + " " + temp.getAbsolutePath();
    +
    +        } else {
    +            // null working directory. Assume want to work in current working directory
    +            batchSubmitCommand = "qsub " + queue + "-S /bin/bash -V -cwd -sync y -t 1-" + commandList.size() + " "
    +                    + temp.getAbsolutePath();
    +        }
    +
    +        boolean success = runProgramAndWait(batchSubmitCommand);
    +
    +        if (success) {
    +            // If no error, delete qsub output and error files
    +            File[] filelist = new File(".").listFiles();
    +            for (File f : filelist) {
    +                if (f.getName().startsWith(temp.getName())) f.delete();
    +            }
    +        }
    +
    +        return success;
    +    }
    +
    +    /**
    +     * Call the grid engine using the parallel environment that has been configured for it. The
    +     * batchType enumeration should contain a number specifying the number of CPUs needed per job.
    +     * Useful when trying to run memory intensive programs such as DistributedGravity.java.
    +     *
    +     * @param commandList
    +     * @return
    +     * @throws InterruptedException
    +     * @throws IOException
    +     */
    +    private boolean runBatchSubmitProgramGridEngineLimitSlots(
    +            List commandList, String workingDir, BatchType batchType) throws InterruptedException, IOException {
    +
    +        // evaluate workingDir. If empty string then set to null;
    +        workingDir = emptyToNull(workingDir);
    +
    +        // Create a text file for input to qsub making use of qsub's job array
    +        // option
    +        File tempDir = null;
    +        if (workingDir != null) {
    +            tempDir = new File(workingDir);
    +        }
    +        File temp = File.createTempFile("altwg-batch-list", ".bash", tempDir);
    +
    +        FileWriter ofs = new FileWriter(temp);
    +        BufferedWriter out = new BufferedWriter(ofs);
    +        out.write("CURRENTDATE=`date +\"%Y-%m-%d %T\"`\n");
    +        out.write("echo $CURRENTDATE The node running this job is $HOSTNAME\n");
    +        for (int i = 1; i <= commandList.size(); ++i)
    +            out.write("if [ $SGE_TASK_ID -eq " + i + " ]; then \n " + commandList.get(i - 1) + "\n exit $?\nfi\n");
    +        out.close();
    +
    +        // default to using 6 cpus per job.
    +        String cpus = "6";
    +        // String batchString = batchType.toString();
    +        int numSlots = BatchType.slotPerJob(batchType);
    +        if (numSlots == 999) {
    +            String errMesg =
    +                    "ERROR! batchType:" + batchType.toString() + " not supported by" + " BatchType.slotsPerJob()!";
    +            throw new RuntimeException(errMesg);
    +        }
    +        cpus = Integer.toString(numSlots);
    +
    +        // check to see if grid queue is specified
    +        StringBuilder batchCmd = new StringBuilder("qsub");
    +        if ((gridQueue != null) && (gridQueue.length() > 1)) {
    +
    +            // use grid queue name explicitly
    +            batchCmd.append(" -q " + gridQueue);
    +        }
    +
    +        // Try to use parallel processing environment
    +        batchCmd.append(" -pe ocmp");
    +        batchCmd.append(" " + cpus);
    +
    +        batchCmd.append(" -S /bin/bash");
    +        batchCmd.append(" -V");
    +
    +        if (workingDir != null) {
    +            batchCmd.append(" -wd " + workingDir);
    +        } else {
    +            batchCmd.append(" -cwd ");
    +        }
    +        batchCmd.append(" -sync y");
    +        batchCmd.append(" -t 1-" + commandList.size());
    +        batchCmd.append(" " + temp.getAbsolutePath());
    +
    +        // batchSubmitCommand = "qsub -pe ocmp " + cpus + " -S /bin/bash -V -cwd -sync y -t 1-" +
    +        // commandList.size() + " "
    +        // + temp.getAbsolutePath();
    +        // batchSubmitCommand = "qsub -pe ocmp " + cpus + " -S /bin/bash -V -wd " + workingDir + " -sync
    +        // y -t 1-" + commandList.size() + " "
    +        // + temp.getAbsolutePath();
    +
    +        String batchSubmitCommand = batchCmd.toString();
    +        boolean success = runProgramAndWait(batchSubmitCommand);
    +
    +        if (success) {
    +            // If no error, delete qsub output and error files
    +            File[] filelist = new File(".").listFiles();
    +            for (File f : filelist) {
    +                if (f.getName().startsWith(temp.getName())) f.delete();
    +            }
    +        }
    +
    +        return success;
    +    }
    +
    +    private boolean runBatchSubmitProgramParallel(List commandList) throws InterruptedException, IOException {
    +        // Create a text file with all the commands that should be run, one per
    +        // line
    +        File temp = File.createTempFile("altwg-batch-list", ".tmp", null);
    +        BatchSubmitI.saveList(commandList, temp.getAbsolutePath());
    +
    +        // Now submit all these batches GNU Parallel
    +        String batchSubmitCommand = "parallel -v -a " + temp.getAbsolutePath();
    +
    +        return runProgramAndWait(batchSubmitCommand);
    +    }
    +
    +    private boolean runBatchSubmitProgramLocalSequential(List commandList, String workingDir)
    +            throws IOException, InterruptedException {
    +
    +        // evaluate workingDir. If empty string then set to null;
    +        workingDir = emptyToNull(workingDir);
    +
    +        boolean successful = true;
    +        for (String command : commandList) {
    +            if (workingDir != null) {
    +                File workingFile = new File(workingDir);
    +                if (!runProgramAndWait(command, workingFile)) successful = false;
    +            } else {
    +                if (!runProgramAndWait(command)) successful = false;
    +            }
    +        }
    +
    +        return successful;
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/batch/BatchType.java b/src/main/java/terrasaur/utils/batch/BatchType.java
    index eedf065..0f7e182 100644
    --- a/src/main/java/terrasaur/utils/batch/BatchType.java
    +++ b/src/main/java/terrasaur/utils/batch/BatchType.java
    @@ -28,111 +28,122 @@ import org.apache.logging.log4j.Logger;
     /**
      * Defines type of processing to be done on a batch of commands. Usually a form of local or
      * clustered network processing.
    - * 
    + *
      * @author espirrc1
      *
      */
     public enum BatchType {
    +    GRID_ENGINE,
    +    GRID_ENGINE_2,
    +    GRID_ENGINE_3,
    +    GRID_ENGINE_4,
    +    GRID_ENGINE_6,
    +    GRID_ENGINE_8,
    +    GRID_ENGINE_10,
    +    GRID_ENGINE_16,
    +    GRID_ENGINE_17,
    +    GRID_ENGINE_23,
    +    GRID_ENGINE_32,
    +    GRID_ENGINE_33,
    +    GRID_ENGINE_35,
    +    GRID_ENGINE_45,
    +    GNU_PARALLEL,
    +    LOCAL_SEQUENTIAL;
     
    -  GRID_ENGINE, GRID_ENGINE_2, GRID_ENGINE_3, GRID_ENGINE_4, GRID_ENGINE_6, GRID_ENGINE_8, GRID_ENGINE_10, GRID_ENGINE_16, GRID_ENGINE_17, GRID_ENGINE_23, GRID_ENGINE_32, GRID_ENGINE_33, GRID_ENGINE_35, GRID_ENGINE_45, GNU_PARALLEL, LOCAL_SEQUENTIAL;
    +    private static Logger logger = LogManager.getLogger(BatchType.class);
     
    -  private static Logger logger = LogManager.getLogger(BatchType.class);
    +    /**
    +     * Return the BatchType appropriate to the desired number of slots per job.
    +     *
    +     * @param numSlotsPerJob
    +     * @return
    +     */
    +    public static BatchType getSlotsType(int numSlotsPerJob) {
     
    -  /**
    -   * Return the BatchType appropriate to the desired number of slots per job.
    -   * 
    -   * @param numSlotsPerJob
    -   * @return
    -   */
    -  public static BatchType getSlotsType(int numSlotsPerJob) {
    -
    -    if (numSlotsPerJob == 2) {
    -      return GRID_ENGINE_2;
    -    } else if (numSlotsPerJob == 3) {
    -      return GRID_ENGINE_3;
    -    } else if (numSlotsPerJob == 4) {
    -      return GRID_ENGINE_4;
    -    } else if (numSlotsPerJob == 6) {
    -      return GRID_ENGINE_6;
    -    } else if (numSlotsPerJob == 8) {
    -      return GRID_ENGINE_8;
    -    } else if (numSlotsPerJob == 10) {
    -      return GRID_ENGINE_10;
    -    } else if (numSlotsPerJob == 16) {
    -      return GRID_ENGINE_16;
    -    } else if (numSlotsPerJob == 17) {
    -      return GRID_ENGINE_17;
    -    } else if (numSlotsPerJob == 23) {
    -      return GRID_ENGINE_23;
    -    } else if (numSlotsPerJob == 32) {
    -      return GRID_ENGINE_32;
    -    } else if (numSlotsPerJob == 33) {
    -      return GRID_ENGINE_33;
    -    } else if (numSlotsPerJob == 35) {
    -      return GRID_ENGINE_35;
    -    } else if (numSlotsPerJob == 45) {
    -      return GRID_ENGINE_45;
    -    } else {
    -      String errMesg = "ERROR! Can only support 2, 3, 4, 6, 8, 10, 16, 17, or 32 slots per job.";
    -      throw new RuntimeException(errMesg);
    -    }
    -  }
    -
    -  /**
    -   * Return the number of slots per job as indicated by the BatchType. Only applicable for
    -   * GRID_ENGINE_# enums. The rest will return 999.
    -   * 
    -   * @param gridBatchType
    -   * @return
    -   */
    -  public static int slotPerJob(BatchType gridBatchType) {
    -    switch (gridBatchType) {
    -      case GRID_ENGINE_2:
    -        return 2;
    -
    -      case GRID_ENGINE_3:
    -        return 3;
    -
    -      case GRID_ENGINE_4:
    -        return 4;
    -
    -      case GRID_ENGINE_6:
    -        return 6;
    -
    -      case GRID_ENGINE_8:
    -        return 8;
    -
    -      case GRID_ENGINE_10:
    -        return 10;
    -
    -      case GRID_ENGINE_16:
    -        return 16;
    -
    -      case GRID_ENGINE_17:
    -        return 17;
    -
    -      case GRID_ENGINE_23:
    -        return 23;
    -
    -      case GRID_ENGINE_32:
    -        return 32;
    -
    -      case GRID_ENGINE_33:
    -        return 33;
    -
    -      case GRID_ENGINE_35:
    -        return 35;
    -
    -      case GRID_ENGINE_45:
    -        return 45;
    -
    -      default:
    -        logger.warn(
    -            "WARNING: slotsPerJob not applicable to BatchType:" + gridBatchType.toString());
    -        logger.warn("returning 999");
    -        return 999;
    +        if (numSlotsPerJob == 2) {
    +            return GRID_ENGINE_2;
    +        } else if (numSlotsPerJob == 3) {
    +            return GRID_ENGINE_3;
    +        } else if (numSlotsPerJob == 4) {
    +            return GRID_ENGINE_4;
    +        } else if (numSlotsPerJob == 6) {
    +            return GRID_ENGINE_6;
    +        } else if (numSlotsPerJob == 8) {
    +            return GRID_ENGINE_8;
    +        } else if (numSlotsPerJob == 10) {
    +            return GRID_ENGINE_10;
    +        } else if (numSlotsPerJob == 16) {
    +            return GRID_ENGINE_16;
    +        } else if (numSlotsPerJob == 17) {
    +            return GRID_ENGINE_17;
    +        } else if (numSlotsPerJob == 23) {
    +            return GRID_ENGINE_23;
    +        } else if (numSlotsPerJob == 32) {
    +            return GRID_ENGINE_32;
    +        } else if (numSlotsPerJob == 33) {
    +            return GRID_ENGINE_33;
    +        } else if (numSlotsPerJob == 35) {
    +            return GRID_ENGINE_35;
    +        } else if (numSlotsPerJob == 45) {
    +            return GRID_ENGINE_45;
    +        } else {
    +            String errMesg = "ERROR! Can only support 2, 3, 4, 6, 8, 10, 16, 17, or 32 slots per job.";
    +            throw new RuntimeException(errMesg);
    +        }
         }
     
    -  }
    +    /**
    +     * Return the number of slots per job as indicated by the BatchType. Only applicable for
    +     * GRID_ENGINE_# enums. The rest will return 999.
    +     *
    +     * @param gridBatchType
    +     * @return
    +     */
    +    public static int slotPerJob(BatchType gridBatchType) {
    +        switch (gridBatchType) {
    +            case GRID_ENGINE_2:
    +                return 2;
     
    +            case GRID_ENGINE_3:
    +                return 3;
    +
    +            case GRID_ENGINE_4:
    +                return 4;
    +
    +            case GRID_ENGINE_6:
    +                return 6;
    +
    +            case GRID_ENGINE_8:
    +                return 8;
    +
    +            case GRID_ENGINE_10:
    +                return 10;
    +
    +            case GRID_ENGINE_16:
    +                return 16;
    +
    +            case GRID_ENGINE_17:
    +                return 17;
    +
    +            case GRID_ENGINE_23:
    +                return 23;
    +
    +            case GRID_ENGINE_32:
    +                return 32;
    +
    +            case GRID_ENGINE_33:
    +                return 33;
    +
    +            case GRID_ENGINE_35:
    +                return 35;
    +
    +            case GRID_ENGINE_45:
    +                return 45;
    +
    +            default:
    +                logger.warn("WARNING: slotsPerJob not applicable to BatchType:" + gridBatchType.toString());
    +                logger.warn("returning 999");
    +                return 999;
    +        }
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/batch/GridType.java b/src/main/java/terrasaur/utils/batch/GridType.java
    index 25d9d3b..a894f4f 100644
    --- a/src/main/java/terrasaur/utils/batch/GridType.java
    +++ b/src/main/java/terrasaur/utils/batch/GridType.java
    @@ -27,83 +27,79 @@ import org.apache.logging.log4j.Logger;
     
     /**
      * Defines the type of grid engine that will be used to do the batch processing.
    - * 
    + *
      * @author espirrc1
      *
      */
     public enum GridType {
    -  
    -  SUNOPENGRID {
    +    SUNOPENGRID {
     
    -    // determine whether SUN open grid environment variable exists
    -    public boolean checkType() {
    -      if (System.getenv("SGE_ROOT") == null) {
    -        logger.error("Sun Open Grid engine environment variable $SGE_ROOT"
    -            + " not found. Cannot run in grid engine. Need to run in local mode.");
    -        return false;
    -      } else {
    -        return true;
    -      }
    -    }
    -  },
    +        // determine whether SUN open grid environment variable exists
    +        public boolean checkType() {
    +            if (System.getenv("SGE_ROOT") == null) {
    +                logger.error("Sun Open Grid engine environment variable $SGE_ROOT"
    +                        + " not found. Cannot run in grid engine. Need to run in local mode.");
    +                return false;
    +            } else {
    +                return true;
    +            }
    +        }
    +    },
     
    -  LOCAL {
    -    // no distributed processing to be done.
    -    // all local processing.
    -    public boolean checkType() {
    -      return true;
    +    LOCAL {
    +        // no distributed processing to be done.
    +        // all local processing.
    +        public boolean checkType() {
    +            return true;
    +        }
    +    };
    +
    +    private static Logger logger = LogManager.getLogger(GridType.class);
    +
    +    public abstract boolean checkType();
    +
    +    /**
    +     * Parse the desired BatchGridType from the input string value. Will return with BatchGridType =
    +     * LOCAL if value could not be parsed.
    +     *
    +     * @param value
    +     * @return
    +     */
    +    public static GridType parseType(String value) {
    +
    +        return parseType(value, false);
         }
     
    -  };
    +    /**
    +     * Parse the desired BatchGridType from the input string value. Will stop with a RuntimeException
    +     * if stopWithError is true.
    +     *
    +     * @param value
    +     * @param stopWithError
    +     * @return
    +     */
    +    public static GridType parseType(String value, boolean stopWithError) {
    +        for (GridType gridType : values()) {
    +            if (gridType.toString().equals(value)) {
    +                return gridType;
    +            }
    +        }
     
    -  private static Logger logger = LogManager.getLogger(GridType.class);
    +        // fall to here if could not parse gridType from String.
    +        StringBuilder sb = new StringBuilder();
    +        sb.append("\n");
    +        sb.append("Warning, could not parse open grid type from" + " string:" + value + ". Possible values are:\n");
    +        for (GridType gridType : values()) {
    +            sb.append(gridType.toString() + "\n");
    +        }
    +        String errMesg = sb.toString();
     
    -  public abstract boolean checkType();
    -
    -  /**
    -   * Parse the desired BatchGridType from the input string value. Will return with BatchGridType =
    -   * LOCAL if value could not be parsed.
    -   * 
    -   * @param value
    -   * @return
    -   */
    -  public static GridType parseType(String value) {
    -
    -    return parseType(value, false);
    -  }
    -
    -  /**
    -   * Parse the desired BatchGridType from the input string value. Will stop with a RuntimeException
    -   * if stopWithError is true.
    -   * 
    -   * @param value
    -   * @param stopWithError
    -   * @return
    -   */
    -  public static GridType parseType(String value, boolean stopWithError) {
    -    for (GridType gridType : values()) {
    -      if (gridType.toString().equals(value)) {
    -        return gridType;
    -      }
    +        if (stopWithError) {
    +            throw new RuntimeException(errMesg);
    +        } else {
    +            logger.warn("Warning, could not parse open grid type from" + " string:" + value);
    +            logger.warn("Will use " + LOCAL.toString());
    +            return LOCAL;
    +        }
         }
    -
    -    // fall to here if could not parse gridType from String.
    -    StringBuilder sb = new StringBuilder();
    -    sb.append("\n");
    -    sb.append("Warning, could not parse open grid type from" + " string:" + value
    -        + ". Possible values are:\n");
    -    for (GridType gridType : values()) {
    -      sb.append(gridType.toString() + "\n");
    -    }
    -    String errMesg = sb.toString();
    -
    -    if (stopWithError) {
    -      throw new RuntimeException(errMesg);
    -    } else {
    -      logger.warn("Warning, could not parse open grid type from" + " string:" + value);
    -      logger.warn("Will use " + LOCAL.toString());
    -      return LOCAL;
    -    }
    -  }
    -
     }
    diff --git a/src/main/java/terrasaur/utils/batch/package-info.java b/src/main/java/terrasaur/utils/batch/package-info.java
    index f127a27..1596a89 100644
    --- a/src/main/java/terrasaur/utils/batch/package-info.java
    +++ b/src/main/java/terrasaur/utils/batch/package-info.java
    @@ -23,4 +23,4 @@
     /**
      * Classes to handle running jobs on a Sun OpenGrid cluster
      */
    -package terrasaur.utils.batch;
    \ No newline at end of file
    +package terrasaur.utils.batch;
    diff --git a/src/main/java/terrasaur/utils/gravity/GravityOptions.java b/src/main/java/terrasaur/utils/gravity/GravityOptions.java
    index 0ebcf9b..fab18a5 100644
    --- a/src/main/java/terrasaur/utils/gravity/GravityOptions.java
    +++ b/src/main/java/terrasaur/utils/gravity/GravityOptions.java
    @@ -28,114 +28,115 @@ import org.immutables.value.Value;
     @Value.Immutable
     public abstract class GravityOptions {
     
    -  public enum ALGORITHM {
    -    WERNER, CHENG
    -  };
    +    public enum ALGORITHM {
    +        WERNER,
    +        CHENG
    +    };
     
    -  public enum EVALUATION {
    -    AVERAGE_VERTICES("--average-vertices"), CENTERS("--centers"), FILE("--file"), VERTICES(
    -        "--vertices");
    +    public enum EVALUATION {
    +        AVERAGE_VERTICES("--average-vertices"),
    +        CENTERS("--centers"),
    +        FILE("--file"),
    +        VERTICES("--vertices");
     
    -    public final String commandString;
    +        public final String commandString;
     
    -    private EVALUATION(String commandString) {
    -      this.commandString = commandString;
    +        private EVALUATION(String commandString) {
    +            this.commandString = commandString;
    +        }
         }
     
    -  }
    +    /** Path to shape model file in OBJ or Gaskell PLT format */
    +    public abstract String plateModelFile();
     
    -  /** Path to shape model file in OBJ or Gaskell PLT format */
    -  public abstract String plateModelFile();
    +    @Value.Default
    +    /** Density of shape model in g/cm^3 (default is 1) */
    +    public double density() {
    +        return 1.;
    +    }
     
    -  @Value.Default
    -  /** Density of shape model in g/cm^3 (default is 1) */
    -  public double density() {
    -    return 1.;
    -  }
    +    @Value.Default
    +    /** Rotation rate of shape model in radians/sec (default is 0) */
    +    public double rotation() {
    +        return 1.;
    +    }
     
    -  @Value.Default
    -  /** Rotation rate of shape model in radians/sec (default is 0) */
    -  public double rotation() {
    -    return 1.;
    -  }
    +    @Value.Default
    +    /**
    +     * gravitational constant to use. Units are (in g cm^3/s^2) (default is 6.67408e-11)
    +     */
    +    public double gravConstant() {
    +        return 6.67408e-11;
    +    }
     
    -  @Value.Default
    -  /**
    -   * gravitational constant to use. Units are (in g cm^3/s^2) (default is 6.67408e-11)
    -   */
    -  public double gravConstant() {
    -    return 6.67408e-11;
    -  }
    +    @Value.Default
    +    /** Algorithm for computing gravity {@link ALGORITHM#WERNER} or {@link ALGORITHM#CHENG} */
    +    public ALGORITHM algorithm() {
    +        return ALGORITHM.WERNER;
    +    }
     
    -  @Value.Default
    -  /** Algorithm for computing gravity {@link ALGORITHM#WERNER} or {@link ALGORITHM#CHENG} */
    -  public ALGORITHM algorithm() {
    -    return ALGORITHM.WERNER;
    -  }
    +    @Value.Default
    +    /** Where to evaluate gravity. Default is {@link EVALUATION#CENTERS} */
    +    public EVALUATION evaluation() {
    +        return EVALUATION.CENTERS;
    +    }
     
    -  @Value.Default
    -  /** Where to evaluate gravity. Default is {@link EVALUATION#CENTERS} */
    -  public EVALUATION evaluation() {
    -    return EVALUATION.CENTERS;
    -  }
    +    /** name of file containing points for gravity evaluation */
    +    public abstract Optional fieldPointsFile();
     
    -  /** name of file containing points for gravity evaluation */
    -  public abstract Optional fieldPointsFile();
    +    /**
    +     * If {@link #evaluation()} is {@link EVALUATION#FILE}, then use this option to specify the
    +     * reference potential (in J/kg) which is needed for calculating elevation. If
    +     * {@link EVALUATION#FILE} is used but refPotential is not set then no elevation data is saved
    +     * out.
    +     */
    +    public abstract Optional refPotential();
     
    -  /**
    -   * If {@link #evaluation()} is {@link EVALUATION#FILE}, then use this option to specify the
    -   * reference potential (in J/kg) which is needed for calculating elevation. If
    -   * {@link EVALUATION#FILE} is used but refPotential is not set then no elevation data is saved
    -   * out.
    -   */
    -  public abstract Optional refPotential();
    +    @Value.Default
    +    /**
    +     * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the x coordinate
    +     * in the input file. Default is 0.
    +     */
    +    public int columnX() {
    +        return 0;
    +    }
     
    -  @Value.Default
    -  /**
    -   * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the x coordinate
    -   * in the input file. Default is 0.
    -   */
    -  public int columnX() {
    -    return 0;
    -  }
    +    @Value.Default
    +    /**
    +     * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the y coordinate
    +     * in the input file. Default is 1.
    +     */
    +    public int columnY() {
    +        return 1;
    +    }
     
    -  @Value.Default
    -  /**
    -   * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the y coordinate
    -   * in the input file. Default is 1.
    -   */
    -  public int columnY() {
    -    return 1;
    -  }
    +    @Value.Default
    +    /**
    +     * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the z coordinate
    +     * in the input file. Default is 2.
    +     */
    +    public int columnZ() {
    +        return 2;
    +    }
     
    -  @Value.Default
    -  /**
    -   * If {@link #evaluation()} is {@link EVALUATION#FILE}, specify default column of the z coordinate
    -   * in the input file. Default is 2.
    -   */
    -  public int columnZ() {
    -    return 2;
    -  }
    +    /** Index of first plate to process. Useful for parallelizing large shape models. Default is 0. */
    +    public abstract Optional startIndex();
     
    -  /** Index of first plate to process. Useful for parallelizing large shape models. Default is 0. */
    -  public abstract Optional startIndex();
    +    /**
    +     * Number of plates to process. Useful for parallelizing large shape models. Default is all
    +     * plates.
    +     */
    +    public abstract Optional numPlates();
     
    -  /**
    -   * Number of plates to process. Useful for parallelizing large shape models. Default is all
    -   * plates.
    -   */
    -  public abstract Optional numPlates();
    -
    -  @Value.Default
    -  /**
    -   * If specified, the suffix will be appended to all output files. This is needed when splitting
    -   * large shape models into multiple runs so that each run will be output to different files.
    -   */
    -  public String suffix() {
    -    return "";
    -  }
    -
    -  /** Path to folder in which to place output files (default is current directory). */
    -  public abstract Optional outputFolder();
    +    @Value.Default
    +    /**
    +     * If specified, the suffix will be appended to all output files. This is needed when splitting
    +     * large shape models into multiple runs so that each run will be output to different files.
    +     */
    +    public String suffix() {
    +        return "";
    +    }
     
    +    /** Path to folder in which to place output files (default is current directory). */
    +    public abstract Optional outputFolder();
     }
    diff --git a/src/main/java/terrasaur/utils/gravity/GravityResult.java b/src/main/java/terrasaur/utils/gravity/GravityResult.java
    index 030e831..742af4a 100644
    --- a/src/main/java/terrasaur/utils/gravity/GravityResult.java
    +++ b/src/main/java/terrasaur/utils/gravity/GravityResult.java
    @@ -24,85 +24,83 @@ package terrasaur.utils.gravity;
     
     public class GravityResult implements Comparable {
     
    -  int index;
    -  double[] xyz;
    -  double area;
    -  double potential;
    -  double[] acc;
    -  double elevation;
    +    int index;
    +    double[] xyz;
    +    double area;
    +    double potential;
    +    double[] acc;
    +    double elevation;
     
    -  public int getIndex() {
    -    return index;
    -  }
    +    public int getIndex() {
    +        return index;
    +    }
     
    -  public double[] getXYZ() {
    -    double[] tmp = new double[3];
    -    System.arraycopy(xyz, 0, tmp, 0, 3);
    -    return tmp;
    -  }
    +    public double[] getXYZ() {
    +        double[] tmp = new double[3];
    +        System.arraycopy(xyz, 0, tmp, 0, 3);
    +        return tmp;
    +    }
     
    -  public double getArea() {
    -    return area;
    -  }
    +    public double getArea() {
    +        return area;
    +    }
     
    -  void setArea(double area) {
    -    this.area = area;
    -  }
    +    void setArea(double area) {
    +        this.area = area;
    +    }
     
    -  public double getPotential() {
    -    return potential;
    -  }
    +    public double getPotential() {
    +        return potential;
    +    }
     
    -  public double[] getAcc() {
    -    double[] tmp = new double[3];
    -    System.arraycopy(acc, 0, tmp, 0, 3);
    -    return tmp;
    -  }
    +    public double[] getAcc() {
    +        double[] tmp = new double[3];
    +        System.arraycopy(acc, 0, tmp, 0, 3);
    +        return tmp;
    +    }
     
    -  public double getElevation() {
    -    return elevation;
    -  }
    +    public double getElevation() {
    +        return elevation;
    +    }
     
    -  void setElevation(double elevation) {
    -    this.elevation = elevation;
    -  }
    +    void setElevation(double elevation) {
    +        this.elevation = elevation;
    +    }
     
    -  GravityResult(int index, double[] xyz, double potential, double[] acc) {
    -    this.index = index;
    -    this.xyz = new double[3];
    -    System.arraycopy(xyz, 0, this.xyz, 0, 3);
    -    this.potential = potential;
    -    this.acc = new double[3];
    -    System.arraycopy(acc, 0, this.acc, 0, 3);
    -    elevation = 0;
    -    area = 1;
    -  }
    +    GravityResult(int index, double[] xyz, double potential, double[] acc) {
    +        this.index = index;
    +        this.xyz = new double[3];
    +        System.arraycopy(xyz, 0, this.xyz, 0, 3);
    +        this.potential = potential;
    +        this.acc = new double[3];
    +        System.arraycopy(acc, 0, this.acc, 0, 3);
    +        elevation = 0;
    +        area = 1;
    +    }
     
    -  @Override
    -  public int compareTo(GravityResult o) {
    -    return Integer.compare(index, o.index);
    -  }
    +    @Override
    +    public int compareTo(GravityResult o) {
    +        return Integer.compare(index, o.index);
    +    }
     
    -  public String toCSV() {
    -    return String.format("%d, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e",
    -        index, xyz[0], xyz[1], xyz[2], area, potential, acc[0], acc[1], acc[2]);
    -  }
    +    public String toCSV() {
    +        return String.format(
    +                "%d, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e, %14.8e",
    +                index, xyz[0], xyz[1], xyz[2], area, potential, acc[0], acc[1], acc[2]);
    +    }
     
    -  public static GravityResult fromCSV(String csv) {
    -    String[] parts = csv.split(",");
    -    int index = Integer.parseInt(parts[0]);
    -    double[] xyz = new double[3];
    -    for (int i = 0; i < 3; i++)
    -      xyz[i] = Double.parseDouble(parts[i + 1]);
    -    double area = Double.parseDouble(parts[4]);
    -    double potential = Double.parseDouble(parts[5]);
    -    double[] acc = new double[3];
    -    for (int i = 0; i < 3; i++)
    -      acc[i] = Double.parseDouble(parts[i + 6]);
    -
    -    GravityResult gr = new GravityResult(index, xyz, potential, acc);
    -    gr.setArea(area);
    -    return gr;
    -  }
    +    public static GravityResult fromCSV(String csv) {
    +        String[] parts = csv.split(",");
    +        int index = Integer.parseInt(parts[0]);
    +        double[] xyz = new double[3];
    +        for (int i = 0; i < 3; i++) xyz[i] = Double.parseDouble(parts[i + 1]);
    +        double area = Double.parseDouble(parts[4]);
    +        double potential = Double.parseDouble(parts[5]);
    +        double[] acc = new double[3];
    +        for (int i = 0; i < 3; i++) acc[i] = Double.parseDouble(parts[i + 6]);
     
    +        GravityResult gr = new GravityResult(index, xyz, potential, acc);
    +        gr.setArea(area);
    +        return gr;
    +    }
     }
    diff --git a/src/main/java/terrasaur/utils/gravity/GravityUtils.java b/src/main/java/terrasaur/utils/gravity/GravityUtils.java
    index e62d456..1d14453 100644
    --- a/src/main/java/terrasaur/utils/gravity/GravityUtils.java
    +++ b/src/main/java/terrasaur/utils/gravity/GravityUtils.java
    @@ -31,81 +31,77 @@ import terrasaur.utils.gravity.GravityOptions.EVALUATION;
     
     public class GravityUtils {
     
    -  private final static Logger logger = LogManager.getLogger(GravityUtils.class);
    +    private static final Logger logger = LogManager.getLogger(GravityUtils.class);
     
    -  /**
    -   * Modify potential due to gravity from external body.
    -   * 

    - * TODO: add estimate for Hill Sphere radius, spherical harmonic expansion for primary - * - * @param plateResults self-gravity acceleration and potential - * @param externalMass mass of perturbing body, kg - * @param externalXYZ position in body fixed coordinates of perturbing body, km - * @param gravConst gravitational constant, m^3/kgs^2 - * @return updated acceleration and potential - */ - public static List addExternalBody(List plateResults, - double externalMass, double[] externalXYZ, double gravConst) { - double totalArea = 0; - for (GravityResult gr : plateResults) - totalArea += gr.getArea(); + /** + * Modify potential due to gravity from external body. + *

    + * TODO: add estimate for Hill Sphere radius, spherical harmonic expansion for primary + * + * @param plateResults self-gravity acceleration and potential + * @param externalMass mass of perturbing body, kg + * @param externalXYZ position in body fixed coordinates of perturbing body, km + * @param gravConst gravitational constant, m^3/kgs^2 + * @return updated acceleration and potential + */ + public static List addExternalBody( + List plateResults, double externalMass, double[] externalXYZ, double gravConst) { + double totalArea = 0; + for (GravityResult gr : plateResults) totalArea += gr.getArea(); - logger.info(String.format( - "Adding contribution from external body. Mass %.3e kg, position %.3e %.3e %.3e km, total area %.3e km^2\n", - externalMass, externalXYZ[0], externalXYZ[1], externalXYZ[2], totalArea)); + logger.info(String.format( + "Adding contribution from external body. Mass %.3e kg, position %.3e %.3e %.3e km, total area %.3e km^2\n", + externalMass, externalXYZ[0], externalXYZ[1], externalXYZ[2], totalArea)); - // first find potential and acceleration at body center - double R = new Vector3D(externalXYZ).getNorm() * 1e3; // meters + // first find potential and acceleration at body center + double R = new Vector3D(externalXYZ).getNorm() * 1e3; // meters - // results are in units of J/kg and m/s^2 - // GM is in SI units - final double GM = gravConst * externalMass; - double gMag = GM / R / R; - double[] gCenter = new double[3]; - double pCenter = -GM / R; - for (int i = 0; i < 3; i++) - gCenter[i] = gMag * externalXYZ[i] * 1e3 / R; + // results are in units of J/kg and m/s^2 + // GM is in SI units + final double GM = gravConst * externalMass; + double gMag = GM / R / R; + double[] gCenter = new double[3]; + double pCenter = -GM / R; + for (int i = 0; i < 3; i++) gCenter[i] = gMag * externalXYZ[i] * 1e3 / R; - List updatedPlateResults = new ArrayList<>(); - for (GravityResult gr : plateResults) { - Vector3D bodyToPoint = new Vector3D(gr.getXYZ()).subtract(new Vector3D(externalXYZ)); - double dist = bodyToPoint.getNorm() * 1e3; // meters - double potential = -GM / dist; + List updatedPlateResults = new ArrayList<>(); + for (GravityResult gr : plateResults) { + Vector3D bodyToPoint = new Vector3D(gr.getXYZ()).subtract(new Vector3D(externalXYZ)); + double dist = bodyToPoint.getNorm() * 1e3; // meters + double potential = -GM / dist; - double[] acc = gr.getAcc(); + double[] acc = gr.getAcc(); - double[] bodyToPointArray = bodyToPoint.toArray(); - for (int i = 0; i < 3; i++) - acc[i] += potential / dist * (bodyToPointArray[i] * 1e3 / dist) - gCenter[i]; - potential -= pCenter; - potential += gr.getPotential(); - updatedPlateResults.add(new GravityResult(gr.getIndex(), gr.getXYZ(), potential, acc)); + double[] bodyToPointArray = bodyToPoint.toArray(); + for (int i = 0; i < 3; i++) acc[i] += potential / dist * (bodyToPointArray[i] * 1e3 / dist) - gCenter[i]; + potential -= pCenter; + potential += gr.getPotential(); + updatedPlateResults.add(new GravityResult(gr.getIndex(), gr.getXYZ(), potential, acc)); + } + + return updatedPlateResults; } - return updatedPlateResults; - } + public static String buildCommand(GravityOptions options) { - public static String buildCommand(GravityOptions options) { + StringBuilder sb = new StringBuilder(); - StringBuilder sb = new StringBuilder(); - - sb.append("gravity "); - sb.append(String.format("-d %.16e ", options.density())); - sb.append(String.format("-r %.16e ", options.rotation())); - sb.append(String.format("--%s ", options.algorithm().name().toLowerCase())); - sb.append(String.format("--%s ", options.evaluation().commandString)); - if (options.evaluation() == EVALUATION.FILE) - sb.append(String.format("%s ", options.fieldPointsFile().get())); - sb.append(String.format("--start-index %d ", options.startIndex().get())); - sb.append( - String.format("--end-index %d ", options.startIndex().get() + options.numPlates().get())); - sb.append(String.format("--suffix %s ", options.suffix())); - sb.append(String.format("--output-folder %s ", options.outputFolder().get())); - sb.append(String.format("-gravConst %.16e ", options.gravConstant())); - sb.append(options.plateModelFile()); - - return sb.toString(); - - } + sb.append("gravity "); + sb.append(String.format("-d %.16e ", options.density())); + sb.append(String.format("-r %.16e ", options.rotation())); + sb.append(String.format("--%s ", options.algorithm().name().toLowerCase())); + sb.append(String.format("--%s ", options.evaluation().commandString)); + if (options.evaluation() == EVALUATION.FILE) + sb.append(String.format("%s ", options.fieldPointsFile().get())); + sb.append(String.format("--start-index %d ", options.startIndex().get())); + sb.append(String.format( + "--end-index %d ", + options.startIndex().get() + options.numPlates().get())); + sb.append(String.format("--suffix %s ", options.suffix())); + sb.append(String.format("--output-folder %s ", options.outputFolder().get())); + sb.append(String.format("-gravConst %.16e ", options.gravConstant())); + sb.append(options.plateModelFile()); + return sb.toString(); + } } diff --git a/src/main/java/terrasaur/utils/lidar/LidarTransformation.java b/src/main/java/terrasaur/utils/lidar/LidarTransformation.java index 7aee4dc..fae1ac3 100644 --- a/src/main/java/terrasaur/utils/lidar/LidarTransformation.java +++ b/src/main/java/terrasaur/utils/lidar/LidarTransformation.java @@ -22,6 +22,9 @@ */ package terrasaur.utils.lidar; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -32,133 +35,125 @@ 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 com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; /** * This class parses the JSON output of lidar-optimize - * + * * @author Hari.Nair@jhuapl.edu * */ public class LidarTransformation { - private final static Logger logger = LogManager.getLogger(LidarTransformation.class); + private static final Logger logger = LogManager.getLogger(LidarTransformation.class); - private List translation; - private List rotation; - private List centerOfRotation; - private int startId; - private int stopId; - private double minErrorBefore; - private double maxErrorBefore; - private double stdBefore; - private double minErrorAfter; - private double maxErrorAfter; - private double rmsAfter; - private double meanErrorAfter; - private double stdAfter; + private List translation; + private List rotation; + private List centerOfRotation; + private int startId; + private int stopId; + private double minErrorBefore; + private double maxErrorBefore; + private double stdBefore; + private double minErrorAfter; + private double maxErrorAfter; + private double rmsAfter; + private double meanErrorAfter; + private double stdAfter; - private Vector3D translationObject; - private Rotation rotationObject; - private Vector3D centerOfRotationObject; + private Vector3D translationObject; + private Rotation rotationObject; + private Vector3D centerOfRotationObject; - /** - * Create a transform with the translation set to {@link Vector3D#ZERO}, rotation to - * {@link Rotation#IDENTITY}, and center of rotation to {@link Vector3D#ZERO}. - * - * @return - */ - public static LidarTransformation defaultTransform() { - LidarTransformation t = new LidarTransformation(); - t.translationObject = Vector3D.ZERO; - t.rotationObject = Rotation.IDENTITY; - t.centerOfRotationObject = Vector3D.ZERO; + /** + * Create a transform with the translation set to {@link Vector3D#ZERO}, rotation to + * {@link Rotation#IDENTITY}, and center of rotation to {@link Vector3D#ZERO}. + * + * @return + */ + public static LidarTransformation defaultTransform() { + LidarTransformation t = new LidarTransformation(); + t.translationObject = Vector3D.ZERO; + t.rotationObject = Rotation.IDENTITY; + t.centerOfRotationObject = Vector3D.ZERO; - return t; - } - - private static LidarTransformation fromJSON(Reader reader) { - Gson gson = new Gson(); - JsonReader jsonReader = new JsonReader(reader); - LidarTransformation object = - gson.fromJson(jsonReader, new TypeToken() {}.getType()); - return object; - } - - /** - * Load a LidarTransformation object from a JSON file. - * - * @param file - * @return - */ - public static LidarTransformation fromJSON(File file) { - FileReader reader = null; - try { - reader = new FileReader(file); - return fromJSON(reader); - } catch (FileNotFoundException e) { - logger.warn(e.getLocalizedMessage()); + return t; } - return null; - } - /** - * Load a LidarTransformation object from a JSON string. - * - * @param string - * @return - */ - public static LidarTransformation fromJSON(String string) { - StringReader reader = new StringReader(string); - return fromJSON(reader); - } - - public Vector3D getCenterOfRotation() { - if (centerOfRotationObject == null) { - centerOfRotationObject = - new Vector3D(centerOfRotation.get(0), centerOfRotation.get(1), centerOfRotation.get(2)); + private static LidarTransformation fromJSON(Reader reader) { + Gson gson = new Gson(); + JsonReader jsonReader = new JsonReader(reader); + LidarTransformation object = gson.fromJson(jsonReader, new TypeToken() {}.getType()); + return object; } - return centerOfRotationObject; - } - public Rotation getRotation() { - if (rotationObject == null) { - rotationObject = - new Rotation(rotation.get(0), rotation.get(1), rotation.get(2), rotation.get(3), true); + /** + * Load a LidarTransformation object from a JSON file. + * + * @param file + * @return + */ + public static LidarTransformation fromJSON(File file) { + FileReader reader = null; + try { + reader = new FileReader(file); + return fromJSON(reader); + } catch (FileNotFoundException e) { + logger.warn(e.getLocalizedMessage()); + } + return null; } - return rotationObject; - } - public Vector3D getTranslation() { - if (translationObject == null) { - translationObject = new Vector3D(translation.get(0), translation.get(1), translation.get(2)); + /** + * Load a LidarTransformation object from a JSON string. + * + * @param string + * @return + */ + public static LidarTransformation fromJSON(String string) { + StringReader reader = new StringReader(string); + return fromJSON(reader); } - return translationObject; - } - /** - * Transform an input point. Steps: - *

      - *
    1. Subtract {@link #getCenterOfRotation()}
    2. - *
    3. Apply {@link #getRotation()}
    4. - *
    5. Add {@link #getCenterOfRotation()}
    6. - *
    7. Add {@link #getTranslation()}
    8. - *
    - * - * @param point - * @return transformed point - */ - public Vector3D transformPoint(Vector3D point) { - Vector3D transformed = point.subtract(getCenterOfRotation()); - transformed = getRotation().applyTo(transformed); - transformed = transformed.add(getCenterOfRotation()); - transformed = transformed.add(getTranslation()); + public Vector3D getCenterOfRotation() { + if (centerOfRotationObject == null) { + centerOfRotationObject = + new Vector3D(centerOfRotation.get(0), centerOfRotation.get(1), centerOfRotation.get(2)); + } + return centerOfRotationObject; + } - return transformed; - } + public Rotation getRotation() { + if (rotationObject == null) { + rotationObject = new Rotation(rotation.get(0), rotation.get(1), rotation.get(2), rotation.get(3), true); + } + return rotationObject; + } + public Vector3D getTranslation() { + if (translationObject == null) { + translationObject = new Vector3D(translation.get(0), translation.get(1), translation.get(2)); + } + return translationObject; + } + + /** + * Transform an input point. Steps: + *
      + *
    1. Subtract {@link #getCenterOfRotation()}
    2. + *
    3. Apply {@link #getRotation()}
    4. + *
    5. Add {@link #getCenterOfRotation()}
    6. + *
    7. Add {@link #getTranslation()}
    8. + *
    + * + * @param point + * @return transformed point + */ + public Vector3D transformPoint(Vector3D point) { + Vector3D transformed = point.subtract(getCenterOfRotation()); + transformed = getRotation().applyTo(transformed); + transformed = transformed.add(getCenterOfRotation()); + transformed = transformed.add(getTranslation()); + + return transformed; + } } - - diff --git a/src/main/java/terrasaur/utils/math/MathConversions.java b/src/main/java/terrasaur/utils/math/MathConversions.java index c19999d..8090eeb 100644 --- a/src/main/java/terrasaur/utils/math/MathConversions.java +++ b/src/main/java/terrasaur/utils/math/MathConversions.java @@ -38,162 +38,160 @@ import spice.basic.Vector3; /** * Utilities to convert between vectors/matrices in Apache Commons Math, Picante, and SPICE. - * + * * @author Hari.Nair@jhuapl.edu * */ public class MathConversions { - private final static Logger logger = LogManager.getLogger(MathConversions.class); + private static final Logger logger = LogManager.getLogger(MathConversions.class); - /** - * Convert an Apache Commons {@link Rotation} to a SPICE {@link Matrix33}. - * - * @param r - * @return - */ - public static Matrix33 toMatrix33(Rotation r) { - try { - SpiceQuaternion q = new SpiceQuaternion(r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); - return q.toMatrix().xpose(); - } catch (SpiceException e) { - logger.warn("Cannot convert Rotation to Matrix33."); + /** + * Convert an Apache Commons {@link Rotation} to a SPICE {@link Matrix33}. + * + * @param r + * @return + */ + public static Matrix33 toMatrix33(Rotation r) { + try { + SpiceQuaternion q = new SpiceQuaternion(r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); + return q.toMatrix().xpose(); + } catch (SpiceException e) { + logger.warn("Cannot convert Rotation to Matrix33."); + } + return null; } - return null; - } - /** - * Convert a Picante {@link RotationMatrixIJK} to a SPICE {@link Matrix33}. - * - * @param r - * @return - */ - public static Matrix33 toMatrix33(UnwritableRotationMatrixIJK r) { - try { - Quaternion q = new Quaternion(r); - VectorIJK v = q.getVector(new VectorIJK()); - return new SpiceQuaternion(q.getScalar(), v.getI(), v.getJ(), v.getK()).toMatrix(); - } catch (SpiceException e) { - logger.warn("Cannot convert RotationMatrixIJK to Matrix33."); + /** + * Convert a Picante {@link RotationMatrixIJK} to a SPICE {@link Matrix33}. + * + * @param r + * @return + */ + public static Matrix33 toMatrix33(UnwritableRotationMatrixIJK r) { + try { + Quaternion q = new Quaternion(r); + VectorIJK v = q.getVector(new VectorIJK()); + return new SpiceQuaternion(q.getScalar(), v.getI(), v.getJ(), v.getK()).toMatrix(); + } catch (SpiceException e) { + logger.warn("Cannot convert RotationMatrixIJK to Matrix33."); + } + return null; } - return null; - } - /** - * Convert a SPICE {@link Matrix33} to a Picante {@link RotationMatrixIJK}. - * - * @param m - * @return - */ - public static RotationMatrixIJK toRotationMatrixIJK(Matrix33 m) { - try { - SpiceQuaternion q = new SpiceQuaternion(m); - return new Quaternion(q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3)) - .getRotation(new RotationMatrixIJK()); - } catch (SpiceException e) { - logger.warn("Cannot convert Matrix33 to RotationMatrixIJK."); + /** + * Convert a SPICE {@link Matrix33} to a Picante {@link RotationMatrixIJK}. + * + * @param m + * @return + */ + public static RotationMatrixIJK toRotationMatrixIJK(Matrix33 m) { + try { + SpiceQuaternion q = new SpiceQuaternion(m); + return new Quaternion(q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3)) + .getRotation(new RotationMatrixIJK()); + } catch (SpiceException e) { + logger.warn("Cannot convert Matrix33 to RotationMatrixIJK."); + } + return null; } - return null; - } - /** - * Convert an Apache Commons {@link Rotation} to a Picante {@link RotationMatrixIJK}. - * - * @param r - * @return - */ - public static RotationMatrixIJK toRotationMatrixIJK(Rotation r) { - Quaternion q = new Quaternion(r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); - return q.getRotation(new RotationMatrixIJK()).transpose(); - } - - /** - * Convert a SPICE {@link Matrix33} to an Apache Commons {@link Rotation}. - * - * @param m - * @return - */ - public static Rotation toRotation(Matrix33 m) { - try { - SpiceQuaternion q = new SpiceQuaternion(m.xpose()); - return new Rotation(q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3), false); - } catch (SpiceException e) { - logger.warn("Cannot convert Matrix33 to Rotation."); + /** + * Convert an Apache Commons {@link Rotation} to a Picante {@link RotationMatrixIJK}. + * + * @param r + * @return + */ + public static RotationMatrixIJK toRotationMatrixIJK(Rotation r) { + Quaternion q = new Quaternion(r.getQ0(), r.getQ1(), r.getQ2(), r.getQ3()); + return q.getRotation(new RotationMatrixIJK()).transpose(); } - return null; - } - /** - * Convert a Picante {@link RotationMatrixIJK} to an Apache Commons {@link Rotation}. - * - * @param m - * @return - */ - public static Rotation toRotation(UnwritableRotationMatrixIJK m) { - Quaternion q = new Quaternion(m.createTranspose()); - VectorIJK v = q.getVector(new VectorIJK()); - return new Rotation(q.getScalar(), v.getI(), v.getJ(), v.getK(), false); - } + /** + * Convert a SPICE {@link Matrix33} to an Apache Commons {@link Rotation}. + * + * @param m + * @return + */ + public static Rotation toRotation(Matrix33 m) { + try { + SpiceQuaternion q = new SpiceQuaternion(m.xpose()); + return new Rotation(q.getElt(0), q.getElt(1), q.getElt(2), q.getElt(3), false); + } catch (SpiceException e) { + logger.warn("Cannot convert Matrix33 to Rotation."); + } + return null; + } - /** - * Convert an Apache Commons {@link Vector3D} to a SPICE {@link Vector3}. - * - * @param v - * @return - */ - public static Vector3 toVector3(Vector3D v) { - return new Vector3(v.toArray()); - } + /** + * Convert a Picante {@link RotationMatrixIJK} to an Apache Commons {@link Rotation}. + * + * @param m + * @return + */ + public static Rotation toRotation(UnwritableRotationMatrixIJK m) { + Quaternion q = new Quaternion(m.createTranspose()); + VectorIJK v = q.getVector(new VectorIJK()); + return new Rotation(q.getScalar(), v.getI(), v.getJ(), v.getK(), false); + } - /** - * Convert a Picante {@link VectorIJK} to a SPICE {@link Vector3}. - * - * @param v - * @return - */ - public static Vector3 toVector3(UnwritableVectorIJK v) { - return new Vector3(v.getI(), v.getJ(), v.getK()); - } + /** + * Convert an Apache Commons {@link Vector3D} to a SPICE {@link Vector3}. + * + * @param v + * @return + */ + public static Vector3 toVector3(Vector3D v) { + return new Vector3(v.toArray()); + } - /** - * Convert a SPICE {@link Vector3} to an Apache Commons {@link Vector3D}. - * - * @param v - * @return - */ - public static Vector3D toVector3D(Vector3 v) { - return new Vector3D(v.toArray()); - } + /** + * Convert a Picante {@link VectorIJK} to a SPICE {@link Vector3}. + * + * @param v + * @return + */ + public static Vector3 toVector3(UnwritableVectorIJK v) { + return new Vector3(v.getI(), v.getJ(), v.getK()); + } - /** - * Convert a Picante {@link VectorIJK} to an Apache Commons {@link Vector3D}. - * - * @param v - * @return - */ - public static Vector3D toVector3D(UnwritableVectorIJK v) { - return new Vector3D(v.getI(), v.getJ(), v.getK()); - } + /** + * Convert a SPICE {@link Vector3} to an Apache Commons {@link Vector3D}. + * + * @param v + * @return + */ + public static Vector3D toVector3D(Vector3 v) { + return new Vector3D(v.toArray()); + } - /** - * Convert a SPICE {@link Vector3} to a Picante {@link VectorIJK}. - * - * @param v - * @return - */ - public static VectorIJK toVectorIJK(Vector3 v) { - return new VectorIJK(v.toArray()); - } - - /** - * Convert an Apache Commons {@link Vector3D} to a Picante {@link VectorIJK}. - * - * @param v - * @return - */ - public static VectorIJK toVectorIJK(Vector3D v) { - return new VectorIJK(v.toArray()); - } + /** + * Convert a Picante {@link VectorIJK} to an Apache Commons {@link Vector3D}. + * + * @param v + * @return + */ + public static Vector3D toVector3D(UnwritableVectorIJK v) { + return new Vector3D(v.getI(), v.getJ(), v.getK()); + } + /** + * Convert a SPICE {@link Vector3} to a Picante {@link VectorIJK}. + * + * @param v + * @return + */ + public static VectorIJK toVectorIJK(Vector3 v) { + return new VectorIJK(v.toArray()); + } + /** + * Convert an Apache Commons {@link Vector3D} to a Picante {@link VectorIJK}. + * + * @param v + * @return + */ + public static VectorIJK toVectorIJK(Vector3D v) { + return new VectorIJK(v.toArray()); + } } diff --git a/src/main/java/terrasaur/utils/math/RotationUtils.java b/src/main/java/terrasaur/utils/math/RotationUtils.java index 2405db0..ec01c64 100644 --- a/src/main/java/terrasaur/utils/math/RotationUtils.java +++ b/src/main/java/terrasaur/utils/math/RotationUtils.java @@ -22,13 +22,13 @@ */ package terrasaur.utils.math; +import com.google.common.collect.ImmutableList; import java.util.List; import java.util.Random; import org.apache.commons.math3.geometry.euclidean.threed.Rotation; import org.apache.commons.math3.geometry.euclidean.threed.RotationConvention; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import org.apache.commons.math3.util.Pair; -import com.google.common.collect.ImmutableList; import terrasaur.utils.VectorUtils; /** @@ -38,331 +38,324 @@ import terrasaur.utils.VectorUtils; */ public class RotationUtils { - /** Value used in {@link Rotation#Rotation(double[][], double)}. */ - public static final double THRESHOLD = 1e-6; + /** Value used in {@link Rotation#Rotation(double[][], double)}. */ + public static final double THRESHOLD = 1e-6; - /** - * Construct a {@link Rotation} matrix representing an orthonormal frame defined by primary and - * secondary vectors. The primary vector defines an axis of the orthonormal frame. Another axis is - * the cross product of the primary and secondary vectors (or secondary and primary vectors, - * following the right hand rule). The second axis is the cross product of the first and third - * axes, again following the right hand rule. - * - *

    This matrix will transform vectors in the old frame to the new one. - * - * @param iRow X axis - * @param jRow Y axis - * @param kRow Z axis - * @return rotation to transform vectors in old frame to new frame - */ - private static Rotation fromBasisVectors(Vector3D iRow, Vector3D jRow, Vector3D kRow) { - double[][] m = new double[3][3]; - m[0] = iRow.toArray(); - m[1] = jRow.toArray(); - m[2] = kRow.toArray(); - Rotation r = new Rotation(m, THRESHOLD); - return r; - } - - /** - * Return a {@link Rotation} to a frame where the X axis is aligned along iRow and the Y axis is - * aligned along the component of jRow that is orthogonal to iRow. - * - *

    iRow and jRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.IprimaryJsecondary(iInOldFrame, jInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_I
    -   * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    -   *
    -   * // this should be iInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I);
    -   *
    -   * 
    - * - * @param iRow X axis - * @param jRow Y axis - * @return rotation to new frame - */ - public static Rotation IprimaryJsecondary(Vector3D iRow, Vector3D jRow) { - Vector3D kRow = iRow.crossProduct(jRow).normalize(); - jRow = kRow.crossProduct(iRow).normalize(); - iRow = iRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a {@link Rotation} to a frame where the X axis is aligned along iRow and the Z axis is - * aligned along the component of kRow that is orthogonal to iRow. - * - *

    iRow and kRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.IprimaryKsecondary(iInOldFrame, kInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_I
    -   * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    -   *
    -   * // this should be iInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I);
    -   *
    -   * 
    - * - * @param iRow - * @param kRow - * @return - */ - public static Rotation IprimaryKsecondary(Vector3D iRow, Vector3D kRow) { - Vector3D jRow = kRow.crossProduct(iRow).normalize(); - kRow = iRow.crossProduct(jRow).normalize(); - iRow = iRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a {@link Rotation} to a frame where the Y axis is aligned along jRow and the X axis is - * aligned along the component of iRow that is orthogonal to jRow. - * - *

    jRow and iRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.JprimaryIsecondary(jInOldFrame, iInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_J
    -   * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    -   *
    -   * // this should be jInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J);
    -   *
    -   * 
    - * - * @param jRow - * @param iRow - * @return - */ - public static Rotation JprimaryIsecondary(Vector3D jRow, Vector3D iRow) { - Vector3D kRow = iRow.crossProduct(jRow).normalize(); - iRow = jRow.crossProduct(kRow).normalize(); - jRow = jRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a {@link Rotation} to a frame where the Y axis is aligned along jRow and the Z axis is - * aligned along the component of kRow that is orthogonal to jRow. - * - *

    jRow and kRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.JprimaryKsecondary(jInOldFrame, kInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_J
    -   * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    -   *
    -   * // this should be jInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J);
    -   *
    -   * 
    - * - * @param jRow - * @param kRow - * @return - */ - public static Rotation JprimaryKsecondary(Vector3D jRow, Vector3D kRow) { - Vector3D iRow = jRow.crossProduct(kRow).normalize(); - kRow = iRow.crossProduct(jRow).normalize(); - jRow = jRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a {@link Rotation} to a frame where the Z axis is aligned along kRow and the X axis is - * aligned along the component of iRow that is orthogonal to kRow. - * - *

    kRow and iRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.KprimaryIsecondary(kInOldFrame, iInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_K
    -   * Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame);
    -   *
    -   * // this should be kInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K);
    -   *
    -   * 
    - * - * @param kRow - * @param iRow - * @return - */ - public static Rotation KprimaryIsecondary(Vector3D kRow, Vector3D iRow) { - Vector3D jRow = kRow.crossProduct(iRow).normalize(); - iRow = jRow.crossProduct(kRow).normalize(); - kRow = kRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a {@link Rotation} to a frame where the Z axis is aligned along kRow and the Y axis is - * aligned along the component of jRow that is orthogonal to kRow. - * - *

    kRow and jRow are not assumed to be normalized. - * - *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link - * Rotation#applyTo(Vector3D)}. For example: - * - *

    -   * Rotation oldFrameToNewFrame = RotationUtils.KprimaryJsecondary(kInOldFrame, jInOldFrame);
    -   *
    -   * // this should be Vector3D.PLUS_K
    -   * Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame);
    -   *
    -   * // this should be kInOldFrame
    -   * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K);
    -   *
    -   * 
    - * - * @param kRow - * @param jRow - * @return - */ - public static Rotation KprimaryJsecondary(Vector3D kRow, Vector3D jRow) { - Vector3D iRow = jRow.crossProduct(kRow).normalize(); - jRow = kRow.crossProduct(iRow).normalize(); - kRow = kRow.normalize(); - return fromBasisVectors(iRow, jRow, kRow); - } - - /** - * Return a random rotation. - * - * @return - */ - public static Rotation randomRotation() { - Vector3D axis = VectorUtils.randomVector(); - double angle = new Random().nextDouble() * 2 * Math.PI; - - // the RotationConvention doesn't matter since it's a random rotation - return new Rotation(axis, angle, RotationConvention.VECTOR_OPERATOR); - } - - /** - * Write a rotation as an angle in degrees and unit axis. The axis will be written consistent with - * {@link RotationConvention#FRAME_TRANSFORM}. - * - * @param r - * @return - */ - public static String rotationToString(Rotation r) { - RotationConvention rc = RotationConvention.FRAME_TRANSFORM; - Vector3D axis = r.getAxis(rc); - double angle = r.getAngle(); - - String s = - String.format( - "%.16f,%.16f,%.16f,%.16f", - Math.toDegrees(angle), axis.getX(), axis.getY(), axis.getZ()); - - return s; - } - - /** - * Returns a rotation matrix. Specify rotation by an angle (degrees) and a 3d rotation axis vector - * separated by commas (no spaces) - * - *

    The Rotation returned will transform a fixed vector between two frames, consistent with - * {@link RotationConvention#FRAME_TRANSFORM}. - * - * @param args - * @return - */ - public static Rotation stringToRotation(String args) { - String[] rotationParams = args.split(","); - double angle = Double.parseDouble(rotationParams[0].trim()); - double[] axis = new double[3]; - for (int i = 0; i < 3; i++) axis[i] = Double.parseDouble(rotationParams[i + 1].trim()); - Rotation rotation = - new Rotation(new Vector3D(axis), Math.toRadians(angle), RotationConvention.FRAME_TRANSFORM); - return rotation; - } - - /** - * Write a translation and rotation to a String containing a 4x4 combined translation/rotation - * matrix. The top left 3x3 matrix is the rotation matrix. The top three entries in the right hand - * column are the translation vector. The bottom row is ignored (but is usually 0 0 0 1). * - * - *

    The Rotation returned will transform a fixed vector between two frames, consistent with - * {@link RotationConvention#FRAME_TRANSFORM}. - * - * @param translation - * @param rotation - * @return - */ - public static List transformToString(Vector3D translation, Rotation rotation) { - double[] transArray = translation.toArray(); - double[][] rotArray = rotation.getMatrix(); - - ImmutableList.Builder builder = ImmutableList.builder(); - for (int i = 0; i < 3; i++) - builder.add( - String.format( - "%.16f %.16f %.16f %.16f", - rotArray[i][0], rotArray[i][1], rotArray[i][2], transArray[i])); - builder.add(String.format("%.16f %.16f %.16f %.16f", 0., 0., 0., 1.)); - - return builder.build(); - } - - /** - * Return a translation and rotation from a file containing a 4x4 combined translation/rotation - * matrix. The top left 3x3 matrix is the rotation matrix. The top three entries in the right hand - * column are the translation vector. The bottom row is ignored (but is usually 0 0 0 1). - * - *

    Any blank lines or lines starting with # will be ignored. - * - *

    The Rotation returned will transform a fixed vector between two frames, consistent with - * {@link RotationConvention#FRAME_TRANSFORM}. - * - * @param lines - * @return - */ - public static Pair stringToTransform(List lines) { - double[][] rotArray = new double[3][3]; - double[] transArray = new double[3]; - int row = 0; - for (String line : lines) { - if (row > 2) break; - String stripped = line.strip(); - if (stripped.length() == 0 || stripped.startsWith("#")) continue; - - String[] parts = lines.get(row).trim().split("\\s+"); - - // Store matrix in row, column order for consistency with - // RotationConvention#FRAME_TRANSFORM. - for (int column = 0; column < 3; column++) - rotArray[row][column] = Double.parseDouble(parts[column].trim()); - - transArray[row] = Double.parseDouble(parts[3].trim()); - row++; + /** + * Construct a {@link Rotation} matrix representing an orthonormal frame defined by primary and + * secondary vectors. The primary vector defines an axis of the orthonormal frame. Another axis is + * the cross product of the primary and secondary vectors (or secondary and primary vectors, + * following the right hand rule). The second axis is the cross product of the first and third + * axes, again following the right hand rule. + * + *

    This matrix will transform vectors in the old frame to the new one. + * + * @param iRow X axis + * @param jRow Y axis + * @param kRow Z axis + * @return rotation to transform vectors in old frame to new frame + */ + private static Rotation fromBasisVectors(Vector3D iRow, Vector3D jRow, Vector3D kRow) { + double[][] m = new double[3][3]; + m[0] = iRow.toArray(); + m[1] = jRow.toArray(); + m[2] = kRow.toArray(); + Rotation r = new Rotation(m, THRESHOLD); + return r; + } + + /** + * Return a {@link Rotation} to a frame where the X axis is aligned along iRow and the Y axis is + * aligned along the component of jRow that is orthogonal to iRow. + * + *

    iRow and jRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.IprimaryJsecondary(iInOldFrame, jInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_I
    +     * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    +     *
    +     * // this should be iInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I);
    +     *
    +     * 
    + * + * @param iRow X axis + * @param jRow Y axis + * @return rotation to new frame + */ + public static Rotation IprimaryJsecondary(Vector3D iRow, Vector3D jRow) { + Vector3D kRow = iRow.crossProduct(jRow).normalize(); + jRow = kRow.crossProduct(iRow).normalize(); + iRow = iRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a {@link Rotation} to a frame where the X axis is aligned along iRow and the Z axis is + * aligned along the component of kRow that is orthogonal to iRow. + * + *

    iRow and kRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.IprimaryKsecondary(iInOldFrame, kInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_I
    +     * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    +     *
    +     * // this should be iInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I);
    +     *
    +     * 
    + * + * @param iRow + * @param kRow + * @return + */ + public static Rotation IprimaryKsecondary(Vector3D iRow, Vector3D kRow) { + Vector3D jRow = kRow.crossProduct(iRow).normalize(); + kRow = iRow.crossProduct(jRow).normalize(); + iRow = iRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a {@link Rotation} to a frame where the Y axis is aligned along jRow and the X axis is + * aligned along the component of iRow that is orthogonal to jRow. + * + *

    jRow and iRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.JprimaryIsecondary(jInOldFrame, iInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_J
    +     * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    +     *
    +     * // this should be jInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J);
    +     *
    +     * 
    + * + * @param jRow + * @param iRow + * @return + */ + public static Rotation JprimaryIsecondary(Vector3D jRow, Vector3D iRow) { + Vector3D kRow = iRow.crossProduct(jRow).normalize(); + iRow = jRow.crossProduct(kRow).normalize(); + jRow = jRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a {@link Rotation} to a frame where the Y axis is aligned along jRow and the Z axis is + * aligned along the component of kRow that is orthogonal to jRow. + * + *

    jRow and kRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.JprimaryKsecondary(jInOldFrame, kInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_J
    +     * Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame);
    +     *
    +     * // this should be jInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J);
    +     *
    +     * 
    + * + * @param jRow + * @param kRow + * @return + */ + public static Rotation JprimaryKsecondary(Vector3D jRow, Vector3D kRow) { + Vector3D iRow = jRow.crossProduct(kRow).normalize(); + kRow = iRow.crossProduct(jRow).normalize(); + jRow = jRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a {@link Rotation} to a frame where the Z axis is aligned along kRow and the X axis is + * aligned along the component of iRow that is orthogonal to kRow. + * + *

    kRow and iRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.KprimaryIsecondary(kInOldFrame, iInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_K
    +     * Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame);
    +     *
    +     * // this should be kInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K);
    +     *
    +     * 
    + * + * @param kRow + * @param iRow + * @return + */ + public static Rotation KprimaryIsecondary(Vector3D kRow, Vector3D iRow) { + Vector3D jRow = kRow.crossProduct(iRow).normalize(); + iRow = jRow.crossProduct(kRow).normalize(); + kRow = kRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a {@link Rotation} to a frame where the Z axis is aligned along kRow and the Y axis is + * aligned along the component of jRow that is orthogonal to kRow. + * + *

    kRow and jRow are not assumed to be normalized. + * + *

    When transforming a {@link Vector3D} from the old frame to the new frame, use {@link + * Rotation#applyTo(Vector3D)}. For example: + * + *

    +     * Rotation oldFrameToNewFrame = RotationUtils.KprimaryJsecondary(kInOldFrame, jInOldFrame);
    +     *
    +     * // this should be Vector3D.PLUS_K
    +     * Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame);
    +     *
    +     * // this should be kInOldFrame
    +     * v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K);
    +     *
    +     * 
    + * + * @param kRow + * @param jRow + * @return + */ + public static Rotation KprimaryJsecondary(Vector3D kRow, Vector3D jRow) { + Vector3D iRow = jRow.crossProduct(kRow).normalize(); + jRow = kRow.crossProduct(iRow).normalize(); + kRow = kRow.normalize(); + return fromBasisVectors(iRow, jRow, kRow); + } + + /** + * Return a random rotation. + * + * @return + */ + public static Rotation randomRotation() { + Vector3D axis = VectorUtils.randomVector(); + double angle = new Random().nextDouble() * 2 * Math.PI; + + // the RotationConvention doesn't matter since it's a random rotation + return new Rotation(axis, angle, RotationConvention.VECTOR_OPERATOR); + } + + /** + * Write a rotation as an angle in degrees and unit axis. The axis will be written consistent with + * {@link RotationConvention#FRAME_TRANSFORM}. + * + * @param r + * @return + */ + public static String rotationToString(Rotation r) { + RotationConvention rc = RotationConvention.FRAME_TRANSFORM; + Vector3D axis = r.getAxis(rc); + double angle = r.getAngle(); + + String s = + String.format("%.16f,%.16f,%.16f,%.16f", Math.toDegrees(angle), axis.getX(), axis.getY(), axis.getZ()); + + return s; + } + + /** + * Returns a rotation matrix. Specify rotation by an angle (degrees) and a 3d rotation axis vector + * separated by commas (no spaces) + * + *

    The Rotation returned will transform a fixed vector between two frames, consistent with + * {@link RotationConvention#FRAME_TRANSFORM}. + * + * @param args + * @return + */ + public static Rotation stringToRotation(String args) { + String[] rotationParams = args.split(","); + double angle = Double.parseDouble(rotationParams[0].trim()); + double[] axis = new double[3]; + for (int i = 0; i < 3; i++) axis[i] = Double.parseDouble(rotationParams[i + 1].trim()); + Rotation rotation = new Rotation(new Vector3D(axis), Math.toRadians(angle), RotationConvention.FRAME_TRANSFORM); + return rotation; + } + + /** + * Write a translation and rotation to a String containing a 4x4 combined translation/rotation + * matrix. The top left 3x3 matrix is the rotation matrix. The top three entries in the right hand + * column are the translation vector. The bottom row is ignored (but is usually 0 0 0 1). * + * + *

    The Rotation returned will transform a fixed vector between two frames, consistent with + * {@link RotationConvention#FRAME_TRANSFORM}. + * + * @param translation + * @param rotation + * @return + */ + public static List transformToString(Vector3D translation, Rotation rotation) { + double[] transArray = translation.toArray(); + double[][] rotArray = rotation.getMatrix(); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < 3; i++) + builder.add(String.format( + "%.16f %.16f %.16f %.16f", rotArray[i][0], rotArray[i][1], rotArray[i][2], transArray[i])); + builder.add(String.format("%.16f %.16f %.16f %.16f", 0., 0., 0., 1.)); + + return builder.build(); + } + + /** + * Return a translation and rotation from a file containing a 4x4 combined translation/rotation + * matrix. The top left 3x3 matrix is the rotation matrix. The top three entries in the right hand + * column are the translation vector. The bottom row is ignored (but is usually 0 0 0 1). + * + *

    Any blank lines or lines starting with # will be ignored. + * + *

    The Rotation returned will transform a fixed vector between two frames, consistent with + * {@link RotationConvention#FRAME_TRANSFORM}. + * + * @param lines + * @return + */ + public static Pair stringToTransform(List lines) { + double[][] rotArray = new double[3][3]; + double[] transArray = new double[3]; + int row = 0; + for (String line : lines) { + if (row > 2) break; + String stripped = line.strip(); + if (stripped.length() == 0 || stripped.startsWith("#")) continue; + + String[] parts = lines.get(row).trim().split("\\s+"); + + // Store matrix in row, column order for consistency with + // RotationConvention#FRAME_TRANSFORM. + for (int column = 0; column < 3; column++) rotArray[row][column] = Double.parseDouble(parts[column].trim()); + + transArray[row] = Double.parseDouble(parts[3].trim()); + row++; + } + return new Pair(new Vector3D(transArray), new Rotation(rotArray, THRESHOLD)); } - return new Pair( - new Vector3D(transArray), new Rotation(rotArray, THRESHOLD)); - } } diff --git a/src/main/java/terrasaur/utils/mesh/TriangularFacet.java b/src/main/java/terrasaur/utils/mesh/TriangularFacet.java index ea73076..6620214 100644 --- a/src/main/java/terrasaur/utils/mesh/TriangularFacet.java +++ b/src/main/java/terrasaur/utils/mesh/TriangularFacet.java @@ -31,172 +31,170 @@ import picante.math.vectorspace.VectorIJK; /** * Class representing a triangular facet with three vertices with cartesian coordinates. - * + * * @author nairah1 * */ public class TriangularFacet { - private final UnwritableVectorIJK vertex1; - private final UnwritableVectorIJK vertex2; - private final UnwritableVectorIJK vertex3; + private final UnwritableVectorIJK vertex1; + private final UnwritableVectorIJK vertex2; + private final UnwritableVectorIJK vertex3; - private UnwritableVectorIJK center; - private UnwritableVectorIJK normal; + private UnwritableVectorIJK center; + private UnwritableVectorIJK normal; - private Double area; - private Double meanEdgeLength; + private Double area; + private Double meanEdgeLength; - /** - * Define a triangle. Vertices are in counterclockwise order. e.g. - * - *

    -   *    1
    -   *   / \
    -   *  2---3
    -   * 
    - * - * The normal points in the direction of (v3-v2)x(v1-v2) so the order is important. - * - * @param vertex1 - * @param vertex2 - * @param vertex3 - */ - public TriangularFacet(UnwritableVectorIJK vertex1, UnwritableVectorIJK vertex2, - UnwritableVectorIJK vertex3) { - this.vertex1 = vertex1; - this.vertex2 = vertex2; - this.vertex3 = vertex3; + /** + * Define a triangle. Vertices are in counterclockwise order. e.g. + * + *
    +     *    1
    +     *   / \
    +     *  2---3
    +     * 
    + * + * The normal points in the direction of (v3-v2)x(v1-v2) so the order is important. + * + * @param vertex1 + * @param vertex2 + * @param vertex3 + */ + public TriangularFacet(UnwritableVectorIJK vertex1, UnwritableVectorIJK vertex2, UnwritableVectorIJK vertex3) { + this.vertex1 = vertex1; + this.vertex2 = vertex2; + this.vertex3 = vertex3; - this.area = null; - this.meanEdgeLength = null; - this.center = null; - this.normal = null; - } - - public List getVertices() { - List vertices = new ArrayList<>(); - vertices.add(vertex1); - vertices.add(vertex2); - vertices.add(vertex3); - return Collections.unmodifiableList(vertices); - } - - public UnwritableVectorIJK getVertex1() { - return vertex1; - } - - public UnwritableVectorIJK getVertex2() { - return vertex2; - } - - public UnwritableVectorIJK getVertex3() { - return vertex3; - } - - /** - * return the average of the three vertices - * - * @return - */ - public UnwritableVectorIJK getCenter() { - if (center == null) { - double x = (vertex1.getI() + vertex2.getI() + vertex3.getI()) / 3; - double y = (vertex1.getJ() + vertex2.getJ() + vertex3.getJ()) / 3; - double z = (vertex1.getK() + vertex2.getK() + vertex3.getK()) / 3; - center = new UnwritableVectorIJK(x, y, z); + this.area = null; + this.meanEdgeLength = null; + this.center = null; + this.normal = null; } - return center; - } - /** - * returns the unitized cross product of (v3-v2)x(v1-v2). Vertices are in counterclockwise order. - * - * @return - */ - public UnwritableVectorIJK getNormal() { - if (normal == null) { - UnwritableVectorIJK edge1 = VectorIJK.subtract(vertex3, vertex2); - UnwritableVectorIJK edge2 = VectorIJK.subtract(vertex1, vertex2); - normal = VectorIJK.cross(edge1, edge2).unitize(); + public List getVertices() { + List vertices = new ArrayList<>(); + vertices.add(vertex1); + vertices.add(vertex2); + vertices.add(vertex3); + return Collections.unmodifiableList(vertices); } - return normal; - } - /** - * Find area with Heron's formula - */ - private void calcArea() { - VectorIJK v = VectorIJK.subtract(vertex2, vertex1); - double a = v.getDot(v); - v = VectorIJK.subtract(vertex3, vertex2); - double b = v.getDot(v); - v = VectorIJK.subtract(vertex1, vertex3); - double c = v.getDot(v); - area = (0.25 * Math.sqrt(Math.abs(4.0 * a * c - (a - b + c) * (a - b + c)))); - - meanEdgeLength = (FastMath.sqrt(a) + FastMath.sqrt(b) + FastMath.sqrt(c)) / 3; - } - - /** - * Get the area of the triangle. - * - * @return - */ - public double getArea() { - if (area == null) { - calcArea(); + public UnwritableVectorIJK getVertex1() { + return vertex1; } - return area.doubleValue(); - } - /** - * Get the mean length of the three edges. - * - * @return - */ - public double getMeanEdgeLength() { - if (meanEdgeLength == null) { - calcArea(); + public UnwritableVectorIJK getVertex2() { + return vertex2; } - return meanEdgeLength.doubleValue(); - } - /** - * Check if the ray intersects the triangle. Algorithm is from - * https://stackoverflow.com/questions/27381119/javafx-8-3d-scene-intersection-point - * - * @param source observer position - * @param ray look direction - * @return - */ - public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { - final double EPS = 1e-12; + public UnwritableVectorIJK getVertex3() { + return vertex3; + } - UnwritableVectorIJK a = getVertex1(); - UnwritableVectorIJK b = getVertex2(); - UnwritableVectorIJK c = getVertex3(); - - UnwritableVectorIJK edge1 = VectorIJK.subtract(b, a); - UnwritableVectorIJK edge2 = VectorIJK.subtract(c, a); - UnwritableVectorIJK pvec = VectorIJK.cross(ray, edge2); - double det = edge1.getDot(pvec); - - if (det <= -EPS || det >= EPS) { - double inv_det = 1 / det; - UnwritableVectorIJK tvec = VectorIJK.subtract(source, a); - double u = tvec.getDot(pvec) * inv_det; - if (u >= 0 && u <= 1) { - UnwritableVectorIJK qvec = VectorIJK.cross(tvec, edge1); - double v = ray.getDot(qvec) * inv_det; - if (v >= 0 && u + v <= 1) { - // double t = c.getDot(qvec) * inv_det; - // System.out.printf("det: %f, t: %f, u: %f, v: %f\n", det, t, u, v); - return true; + /** + * return the average of the three vertices + * + * @return + */ + public UnwritableVectorIJK getCenter() { + if (center == null) { + double x = (vertex1.getI() + vertex2.getI() + vertex3.getI()) / 3; + double y = (vertex1.getJ() + vertex2.getJ() + vertex3.getJ()) / 3; + double z = (vertex1.getK() + vertex2.getK() + vertex3.getK()) / 3; + center = new UnwritableVectorIJK(x, y, z); } - } + return center; } - return false; - } + /** + * returns the unitized cross product of (v3-v2)x(v1-v2). Vertices are in counterclockwise order. + * + * @return + */ + public UnwritableVectorIJK getNormal() { + if (normal == null) { + UnwritableVectorIJK edge1 = VectorIJK.subtract(vertex3, vertex2); + UnwritableVectorIJK edge2 = VectorIJK.subtract(vertex1, vertex2); + normal = VectorIJK.cross(edge1, edge2).unitize(); + } + return normal; + } + + /** + * Find area with Heron's formula + */ + private void calcArea() { + VectorIJK v = VectorIJK.subtract(vertex2, vertex1); + double a = v.getDot(v); + v = VectorIJK.subtract(vertex3, vertex2); + double b = v.getDot(v); + v = VectorIJK.subtract(vertex1, vertex3); + double c = v.getDot(v); + area = (0.25 * Math.sqrt(Math.abs(4.0 * a * c - (a - b + c) * (a - b + c)))); + + meanEdgeLength = (FastMath.sqrt(a) + FastMath.sqrt(b) + FastMath.sqrt(c)) / 3; + } + + /** + * Get the area of the triangle. + * + * @return + */ + public double getArea() { + if (area == null) { + calcArea(); + } + return area.doubleValue(); + } + + /** + * Get the mean length of the three edges. + * + * @return + */ + public double getMeanEdgeLength() { + if (meanEdgeLength == null) { + calcArea(); + } + return meanEdgeLength.doubleValue(); + } + + /** + * Check if the ray intersects the triangle. Algorithm is from + * https://stackoverflow.com/questions/27381119/javafx-8-3d-scene-intersection-point + * + * @param source observer position + * @param ray look direction + * @return + */ + public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { + final double EPS = 1e-12; + + UnwritableVectorIJK a = getVertex1(); + UnwritableVectorIJK b = getVertex2(); + UnwritableVectorIJK c = getVertex3(); + + UnwritableVectorIJK edge1 = VectorIJK.subtract(b, a); + UnwritableVectorIJK edge2 = VectorIJK.subtract(c, a); + UnwritableVectorIJK pvec = VectorIJK.cross(ray, edge2); + double det = edge1.getDot(pvec); + + if (det <= -EPS || det >= EPS) { + double inv_det = 1 / det; + UnwritableVectorIJK tvec = VectorIJK.subtract(source, a); + double u = tvec.getDot(pvec) * inv_det; + if (u >= 0 && u <= 1) { + UnwritableVectorIJK qvec = VectorIJK.cross(tvec, edge1); + double v = ray.getDot(qvec) * inv_det; + if (v >= 0 && u + v <= 1) { + // double t = c.getDot(qvec) * inv_det; + // System.out.printf("det: %f, t: %f, u: %f, v: %f\n", det, t, u, v); + return true; + } + } + } + return false; + } } diff --git a/src/main/java/terrasaur/utils/mesh/TriangularMesh.java b/src/main/java/terrasaur/utils/mesh/TriangularMesh.java index 9e00514..e16263a 100644 --- a/src/main/java/terrasaur/utils/mesh/TriangularMesh.java +++ b/src/main/java/terrasaur/utils/mesh/TriangularMesh.java @@ -53,819 +53,816 @@ import terrasaur.utils.octree.Octree; /** * Class representing a mesh made of triangular facets. Implements the {@link Surface} interface. - * + * * @author nairah1 * */ public class TriangularMesh implements Surface { - private static Logger logger = LogManager.getLogger(TriangularMesh.class); + private static Logger logger = LogManager.getLogger(TriangularMesh.class); - private int octreeLevel; - private ThreadLocal threadLocalOctree; + private int octreeLevel; + private ThreadLocal threadLocalOctree; - /** list of 3D vertices in the order they were added */ - private List vertexList; + /** list of 3D vertices in the order they were added */ + private List vertexList; - /** Lookup vertex index from 3D position */ - private Map vertexIndexMap; + /** Lookup vertex index from 3D position */ + private Map vertexIndexMap; - /** List of facets in the order they were added */ - private List facetList; + /** List of facets in the order they were added */ + private List facetList; - /** Lookup facet index from facet */ - private Map facetIndexMap; + /** Lookup facet index from facet */ + private Map facetIndexMap; - /** Lookup facets containing a vertex */ - private Map> vertexFacetMap; + /** Lookup facets containing a vertex */ + private Map> vertexFacetMap; - /** Coordinates of mesh center */ - private UnwritableVectorIJK center; + /** Coordinates of mesh center */ + private UnwritableVectorIJK center; - /** Facets in each level of the octree */ - private Map> facetOctreeMap; + /** Facets in each level of the octree */ + private Map> facetOctreeMap; - // "set" methods violate the Builder pattern, but this allows the color/albedo - // map to change between renderings - private Map albedoMap = Collections.emptyMap(); - private Map colorMap = Collections.emptyMap(); + // "set" methods violate the Builder pattern, but this allows the color/albedo + // map to change between renderings + private Map albedoMap = Collections.emptyMap(); + private Map colorMap = Collections.emptyMap(); - /** - * Get the stored albedo map - * - * @return - */ - public Map getAlbedoMap() { - return albedoMap; - } + /** + * Get the stored albedo map + * + * @return + */ + public Map getAlbedoMap() { + return albedoMap; + } - /** - * Store an albedo map - * - * @param albedoMap - */ - public void setAlbedoMap(Map albedoMap) { - this.albedoMap = albedoMap; - } + /** + * Store an albedo map + * + * @param albedoMap + */ + public void setAlbedoMap(Map albedoMap) { + this.albedoMap = albedoMap; + } - /** - * Get the stored color map - * - * @return - */ - public Map getColorMap() { - return colorMap; - } + /** + * Get the stored color map + * + * @return + */ + public Map getColorMap() { + return colorMap; + } - /** - * Store a color map - * - * @param colorMap - */ - public void setColorMap(Map colorMap) { - this.colorMap = colorMap; - } + /** + * Store a color map + * + * @param colorMap + */ + public void setColorMap(Map colorMap) { + this.colorMap = colorMap; + } - /** - * Compare facets by distance from a center facet - * - * @param center - * @return - */ - public final Comparator FACET_DISTANCE_COMPARATOR(TriangularFacet center) { - Comparator comparator = new Comparator() { + /** + * Compare facets by distance from a center facet + * + * @param center + * @return + */ + public final Comparator FACET_DISTANCE_COMPARATOR(TriangularFacet center) { + Comparator comparator = new Comparator() { - @Override - public int compare(TriangularFacet o1, TriangularFacet o2) { - double dist1 = center.getCenter().getDistance(o1.getCenter()); - double dist2 = center.getCenter().getDistance(o2.getCenter()); - return Double.compare(dist1, dist2); - } + @Override + public int compare(TriangularFacet o1, TriangularFacet o2) { + double dist1 = center.getCenter().getDistance(o1.getCenter()); + double dist2 = center.getCenter().getDistance(o2.getCenter()); + return Double.compare(dist1, dist2); + } + }; + return comparator; + } - }; - return comparator; - } - - /** - * Compare facets by index - */ - public final Comparator FACET_INDEX_COMPARATOR = - new Comparator() { + /** + * Compare facets by index + */ + public final Comparator FACET_INDEX_COMPARATOR = new Comparator() { @Override public int compare(TriangularFacet o1, TriangularFacet o2) { - int index1 = getFacetIndexMap().get(o1); - int index2 = getFacetIndexMap().get(o2); - return Integer.compare(index1, index2); + int index1 = getFacetIndexMap().get(o1); + int index2 = getFacetIndexMap().get(o2); + return Integer.compare(index1, index2); } - - }; - - /** - * Compare vertices by distance from a center point - * - * @param center - * @return - */ - public final Comparator VERTEX_DISTANCE_COMPARATOR( - UnwritableVectorIJK center) { - Comparator comparator = new Comparator() { - - @Override - public int compare(UnwritableVectorIJK o1, UnwritableVectorIJK o2) { - double dist1 = center.getDistance(o1); - double dist2 = center.getDistance(o2); - return Double.compare(dist1, dist2); - } - }; - return comparator; - } - - /** Bounding box containing the shape model */ - private BoundingBox boundingBox; - - /** - * - * @param lines - * @return - */ - public static TriangularMesh fromLines(List lines) { - Builder builder = new Builder(); - for (String line : lines) { - String trimmed = line.trim(); - if (trimmed.length() == 0) { - continue; - } - if (trimmed.startsWith("#")) { - continue; - } - String[] parts = trimmed.split("\\s+"); - if (parts[0].equalsIgnoreCase("v")) { - builder.addVertex(new UnwritableVectorIJK(Double.parseDouble(parts[1]), - Double.parseDouble(parts[2]), Double.parseDouble(parts[3]))); - } - if (parts[0].equalsIgnoreCase("f")) { - int[] indices = new int[3]; - for (int i = 0; i < 3; i++) { - String[] subparts = parts[i + 1].split("/"); - indices[i] = Integer.parseInt(subparts[0]) - 1; - } - builder.addFacet(indices[0], indices[1], indices[2]); - } - - } - logger.debug("read {} vertices and {} facets", builder.vertexList.size(), - builder.facetIndexList.size()); - - return builder.build(); - - } - - /** - * Read a shape model in Wavefront OBJ format. - * - * @param objFile - * @return - */ - public static TriangularMesh readOBJ(String objFile) { - return readOBJ(new File(objFile)); - } - - /** - * Read a shape model in Wavefront OBJ format. - * - * @param objFile - * @return - */ - public static TriangularMesh readOBJ(File objFile) { - List lines = new ArrayList<>(); - try (BufferedReader reader = new BufferedReader(new FileReader(objFile))) { - String line = reader.readLine(); - while (line != null) { - lines.add(line); - line = reader.readLine(); - } - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - return null; - } - - return fromLines(lines); - } - - public void writeOBJ(String objFile) { - - try (PrintWriter pw = new PrintWriter(objFile)) { - - for (UnwritableVectorIJK v : vertexList) { - pw.printf("v %f %f %f\n", v.getI(), v.getJ(), v.getK()); - } - - for (TriangularFacet f : facetList) { - int index1 = getVertexIndexMap().get(f.getVertex1()) + 1; - int index2 = getVertexIndexMap().get(f.getVertex2()) + 1; - int index3 = getVertexIndexMap().get(f.getVertex3()) + 1; - pw.printf("f %d %d %d\n", index1, index2, index3); - } - - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage()); - } - - } - - public static class Builder { - private List vertexList; - private List facetIndexList; - private List facetList; - - private Map vertexIndexMap; - private Map facetIndexMap; - - private Map> vertexFacetMap; - - public Builder() { - this.vertexList = new ArrayList<>(); - this.facetIndexList = new ArrayList<>(); - this.facetList = new ArrayList<>(); - - this.vertexIndexMap = new HashMap<>(); - this.facetIndexMap = new HashMap<>(); - this.vertexFacetMap = new HashMap<>(); - } /** - * Add a vertex to the mesh. - * - * @param vertex - */ - public void addVertex(UnwritableVectorIJK vertex) { - vertexIndexMap.put(vertex, vertexList.size()); - vertexList.add(vertex); - } - - /** - * Add a facet to the mesh. - * - * @param index1 index of vertex 1 - * @param index2 index of vertex 2 - * @param index3 index of vertex 3 - */ - public void addFacet(int index1, int index2, int index3) { - facetIndexList.add(new UnwritableVectorIJK(index1, index2, index3)); - List vertices = new ArrayList<>(); - vertices.add(vertexList.get(index1)); - vertices.add(vertexList.get(index2)); - vertices.add(vertexList.get(index3)); - - TriangularFacet tf = new TriangularFacet(vertices.get(0), vertices.get(1), vertices.get(2)); - facetIndexMap.put(tf, facetList.size()); - facetList.add(tf); - - for (UnwritableVectorIJK vertex : vertices) { - List facets = vertexFacetMap.get(vertex); - if (facets == null) { - facets = new ArrayList<>(); - vertexFacetMap.put(vertex, facets); - } - facets.add(tf); - } - - } - - /** - * Return the mesh - * + * Compare vertices by distance from a center point + * + * @param center * @return */ - public TriangularMesh build() { - TriangularMesh mesh = new TriangularMesh(); - mesh.vertexList = Collections.unmodifiableList(vertexList); - mesh.facetList = Collections.unmodifiableList(facetList); + public final Comparator VERTEX_DISTANCE_COMPARATOR(UnwritableVectorIJK center) { + Comparator comparator = new Comparator() { - Statistics.Builder builder = Statistics.builder(); - for (TriangularFacet f : facetList) { - builder.accumulate(f.getMeanEdgeLength()); - } - Statistics edgeStats = builder.build(); - - mesh.vertexIndexMap = Collections.unmodifiableMap(vertexIndexMap); - mesh.facetIndexMap = Collections.unmodifiableMap(facetIndexMap); - - mesh.vertexFacetMap = Collections.unmodifiableMap(vertexFacetMap); - - Statistics.Builder xBuilder = Statistics.builder(); - Statistics.Builder yBuilder = Statistics.builder(); - Statistics.Builder zBuilder = Statistics.builder(); - - for (UnwritableVectorIJK v : vertexList) { - xBuilder.accumulate(v.getI()); - yBuilder.accumulate(v.getJ()); - zBuilder.accumulate(v.getK()); - } - Statistics xStats = xBuilder.build(); - Statistics yStats = yBuilder.build(); - Statistics zStats = zBuilder.build(); - - mesh.center = new UnwritableVectorIJK(xStats.getMean(), yStats.getMean(), zStats.getMean()); - mesh.boundingBox = new BoundingBox( - new UnwritableVectorIJK(xStats.getMinimumValue(), yStats.getMinimumValue(), - zStats.getMinimumValue()), - new UnwritableVectorIJK(xStats.getMaximumValue(), yStats.getMaximumValue(), - zStats.getMaximumValue())); - mesh.threadLocalOctree = new ThreadLocal<>(); - - // this ratio should be about 10 - double maxSide = Math.max(mesh.boundingBox.getxRange().getLength(), Math - .max(mesh.boundingBox.getyRange().getLength(), mesh.boundingBox.getzRange().getLength())); - - // octree levels above 10 require box indices to be long - int octreeLevel = Math.min(10, - (int) (Math.log(maxSide / (10 * edgeStats.getMean())) / Math.log(2.0) + 0.5)); - double boxSize = maxSide / Math.pow(2, octreeLevel); - logger.printf(Level.DEBUG, - "Octree level %d, Mean edge length %f, box size %f, box size/edge length %f", octreeLevel, - edgeStats.getMean(), boxSize, boxSize / edgeStats.getMean()); - mesh.buildFacetOctreeMap(octreeLevel); - return mesh; - } - } - - private Octree getOctree() { - Octree octree = threadLocalOctree.get(); - if (octree == null) { - octree = new Octree(boundingBox); - threadLocalOctree.set(octree); - } - return octree; - } - - /** - * Return a list of vertices in the order they were added. - * - * @return - */ - public List getVertexList() { - return Collections.unmodifiableList(vertexList); - } - - /** - * return a list of facets in the order they were added - * - * @return - */ - public List getFacetList() { - return Collections.unmodifiableList(facetList); - } - - /** - * Return a mapping from vertex to list index. Add 1 to this index to get the corresponding OBJ - * vertex number (which start from 1). - * - * @return - */ - public Map getVertexIndexMap() { - return vertexIndexMap; - } - - /** - * return a mapping from facet to list index - * - * @return - */ - public Map getFacetIndexMap() { - return facetIndexMap; - } - - /** - * Return a list of facets containing this vertex - * - * @return - */ - public Map> getVertexFacetMap() { - return vertexFacetMap; - } - - /** - * return the mean of all vertex positions - * - * @return - */ - public UnwritableVectorIJK getCenter() { - return center; - } - - /** - * return a mapping of octree box index to {@link Set} of facets contained in the box - * - * @return - */ - public Map> getFacetOctreeMap() { - return facetOctreeMap; - } - - /** - * Get the {@link BoundingBox} which encloses the mesh with its edges parallel to the coordinate - * system axes. - * - * @return - */ - public BoundingBox getBoundingBox() { - return boundingBox; - } - - private void buildFacetOctreeMap(int level) { - facetOctreeMap = new HashMap<>(); - octreeLevel = level; - for (TriangularFacet facet : facetList) { - for (UnwritableVectorIJK v : facet.getVertices()) { - int index = getOctree().getIndex(v, level); - Set facets = facetOctreeMap.get(index); - if (facets == null) { - facets = new HashSet<>(); - facetOctreeMap.put(index, facets); - } - facets.add(facet); - } - } - } - - /** - * Return a set of facets sharing a vertex with this one - * - * @return - */ - public Set getFacetNeighbors(TriangularFacet tf) { - Set facets = new TreeSet<>(FACET_DISTANCE_COMPARATOR(tf)); - - for (UnwritableVectorIJK vertex : tf.getVertices()) { - facets.addAll(vertexFacetMap.get(vertex)); - } - return Collections.unmodifiableSet(facets); - } - - /** - * Return a contiguous set of facets with centers less than radius from this facet's center - * - * @return - */ - public Set getFacetsWithinRadius(TriangularFacet tf, double radius) { - Set facets = new TreeSet<>(FACET_DISTANCE_COMPARATOR(tf)); - facets.add(tf); - - UnwritableVectorIJK center = tf.getCenter(); - - while (true) { - - Set currentFacets = new HashSet<>(facets); - int facetSize = currentFacets.size(); - - for (TriangularFacet facet : currentFacets) { - for (TriangularFacet neighbor : getFacetNeighbors(facet)) { - if (!facets.contains(neighbor)) { - if (neighbor.getCenter().getDistance(center) < radius) { - facets.add(neighbor); + @Override + public int compare(UnwritableVectorIJK o1, UnwritableVectorIJK o2) { + double dist1 = center.getDistance(o1); + double dist2 = center.getDistance(o2); + return Double.compare(dist1, dist2); } - } - } - } - - // we're done if we haven't added any more facets in this loop - if (facets.size() == facetSize) { - break; - } - + }; + return comparator; } - return Collections.unmodifiableSet(facets); - } + /** Bounding box containing the shape model */ + private BoundingBox boundingBox; - /** - * Return a contiguous set of facets with centers within a specified radius of a point. The point - * does not have to be on the shape model. This probably won't work well for points that are - * inside the shape model. - * - * @param point - * @param radius - * @return - */ - public Set getFacetsWithinRadius(UnwritableVectorIJK point, double radius) { - - Set vertices = getVerticesWithinRadius(point, radius); - Set facets = new TreeSet<>(new Comparator() { - - @Override - public int compare(TriangularFacet o1, TriangularFacet o2) { - double dist1 = point.getDistance(o1.getCenter()); - double dist2 = point.getDistance(o2.getCenter()); - return Double.compare(dist1, dist2); - } - - }); - - for (UnwritableVectorIJK v : vertices) { - List facetList = vertexFacetMap.get(v); - for (TriangularFacet facet : facetList) { - if (!facets.contains(facet)) { - if (facet.getCenter().getDistance(point) < radius) { - facets.add(facet); - } - } - } - } - - return facets; - } - - /** - * Return all vertices of a contiguous set of facets within a specified radius of a point. The - * point does not have to be on the shape model. This probably won't work well for points that are - * inside the shape model. - * - * @param point - * @param radius - * @return - */ - public Set getVerticesWithinRadius(UnwritableVectorIJK point, - double radius) { - - Set vertices = new TreeSet<>(VERTEX_DISTANCE_COMPARATOR(point)); - - NavigableSet facets = - getIntersections(getCenter(), VectorIJK.subtract(point, getCenter())); - - while (true) { - - Set currentFacets = new HashSet<>(facets); - int facetSize = currentFacets.size(); - - for (TriangularFacet facet : currentFacets) { - for (TriangularFacet neighbor : getFacetNeighbors(facet)) { - for (UnwritableVectorIJK vertex : neighbor.getVertices()) { - if (!vertices.contains(vertex)) { - if (vertex.getDistance(point) < radius) { - vertices.add(vertex); - facets.add(neighbor); - } + /** + * + * @param lines + * @return + */ + public static TriangularMesh fromLines(List lines) { + Builder builder = new Builder(); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.length() == 0) { + continue; + } + if (trimmed.startsWith("#")) { + continue; + } + String[] parts = trimmed.split("\\s+"); + if (parts[0].equalsIgnoreCase("v")) { + builder.addVertex(new UnwritableVectorIJK( + Double.parseDouble(parts[1]), Double.parseDouble(parts[2]), Double.parseDouble(parts[3]))); + } + if (parts[0].equalsIgnoreCase("f")) { + int[] indices = new int[3]; + for (int i = 0; i < 3; i++) { + String[] subparts = parts[i + 1].split("/"); + indices[i] = Integer.parseInt(subparts[0]) - 1; + } + builder.addFacet(indices[0], indices[1], indices[2]); } - } } - } - - // we're done if we haven't added any more facets in this loop - if (facets.size() == facetSize) { - break; - } + logger.debug("read {} vertices and {} facets", builder.vertexList.size(), builder.facetIndexList.size()); + return builder.build(); } - return vertices; - } - - /** - * Apply the rotation about the center of the shape model - * - * @param rotate - * @return - */ - public TriangularMesh rotate(UnwritableRotationMatrixIJK rotate) { - Builder builder = new Builder(); - UnwritableVectorIJK center = getCenter(); - for (UnwritableVectorIJK v : vertexList) { - VectorIJK rotatedVector = VectorIJK.add(rotate.mxv(VectorIJK.subtract(v, center)), center); - builder.addVertex(rotatedVector); + /** + * Read a shape model in Wavefront OBJ format. + * + * @param objFile + * @return + */ + public static TriangularMesh readOBJ(String objFile) { + return readOBJ(new File(objFile)); } - for (TriangularFacet f : facetList) { - builder.addFacet(vertexList.indexOf(f.getVertex1()), vertexList.indexOf(f.getVertex2()), - vertexList.indexOf(f.getVertex3())); - } - - return builder.build(); - } - - /** - * Return a new Triangular mesh scaled by the specified value - * - * @param scale - * @return - */ - public TriangularMesh scale(double scale) { - - Builder builder = new Builder(); - UnwritableVectorIJK center = getCenter(); - for (UnwritableVectorIJK v : vertexList) { - VectorIJK scaledVector = VectorIJK.add(VectorIJK.subtract(v, center).scale(scale), center); - builder.addVertex(scaledVector); - } - - for (TriangularFacet f : facetList) { - builder.addFacet(vertexList.indexOf(f.getVertex1()), vertexList.indexOf(f.getVertex2()), - vertexList.indexOf(f.getVertex3())); - } - - return builder.build(); - } - - /** - * Create a mesh from the supplied facets - * - * @param facets - * @return - */ - public TriangularMesh subset(Collection facets) { - - Set vertexSet = new HashSet<>(); - for (TriangularFacet facet : facets) { - vertexSet.addAll(facet.getVertices()); - } - - List vertexList = new ArrayList<>(); - vertexList.addAll(vertexSet); - - Builder builder = new Builder(); - for (UnwritableVectorIJK vertex : vertexList) { - builder.addVertex(vertex); - } - for (TriangularFacet facet : facets) { - builder.addFacet(vertexList.indexOf(facet.getVertex1()), - vertexList.indexOf(facet.getVertex2()), vertexList.indexOf(facet.getVertex3())); - } - - return builder.build(); - } - - /** - * Return a new Triangular mesh translated by the specified vector - * - * @param translate - * @return - */ - public TriangularMesh translate(UnwritableVectorIJK translate) { - - Builder builder = new Builder(); - for (UnwritableVectorIJK v : vertexList) { - VectorIJK translatedVector = VectorIJK.add(v, translate); - builder.addVertex(translatedVector); - } - - for (TriangularFacet f : facetList) { - builder.addFacet(vertexList.indexOf(f.getVertex1()), vertexList.indexOf(f.getVertex2()), - vertexList.indexOf(f.getVertex3())); - } - - return builder.build(); - } - - /** - * Calculate facets intersected by the ray with its vertex at origin and pointing towards - * direction. Facets are ordered by distance to origin. If the origin is on a facet, check that - * the first intersection is not the facet containing the origin. - * - * @param origin - * @param direction - * @return - */ - public NavigableSet getIntersections(UnwritableVectorIJK origin, - UnwritableVectorIJK direction) { - - /*- - List list = facetList.parallelStream() - .filter(f -> lookDir.getDirection().getDot(VectorIJK.subtract(f.getCenter(), - getCenter())) < 0 - && f.intersects(lookDir)) - .collect(Collectors.toList()); - */ - - // if the origin is inside the bounding box, do a check to ensure that any - // intersections are along the specified direction - final boolean outsideBox = !getOctree().getBoundingBox(0).closedContains(origin); - - // find the list of bounding boxes intersected by the ray - Set boxIndices = new HashSet<>(); - boxIndices.add(0); - for (int level = 0; level < octreeLevel; level++) { - Set nextIndices = new HashSet<>(); - for (int index : boxIndices) { - if (getOctree().getBoundingBox(index).intersects(origin, direction)) { - nextIndices.add(index); + /** + * Read a shape model in Wavefront OBJ format. + * + * @param objFile + * @return + */ + public static TriangularMesh readOBJ(File objFile) { + List lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(objFile))) { + String line = reader.readLine(); + while (line != null) { + lines.add(line); + line = reader.readLine(); + } + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + return null; } - } - boxIndices = new HashSet<>(); - for (int index : nextIndices) { - boxIndices.addAll(getOctree().contains(index)); - } + + return fromLines(lines); } - // intersection with set of boxes containing facets - boxIndices.retainAll(facetOctreeMap.keySet()); - // now collect all the facets intersected by the ray, sorted by distance to the - // origin - NavigableSet intersectingFacets = - new TreeSet<>(new Comparator() { - @Override - public int compare(TriangularFacet o1, TriangularFacet o2) { - return Double.compare(VectorIJK.subtract(o1.getCenter(), origin).getLength(), - VectorIJK.subtract(o2.getCenter(), origin).getLength()); - } + public void writeOBJ(String objFile) { + try (PrintWriter pw = new PrintWriter(objFile)) { + + for (UnwritableVectorIJK v : vertexList) { + pw.printf("v %f %f %f\n", v.getI(), v.getJ(), v.getK()); + } + + for (TriangularFacet f : facetList) { + int index1 = getVertexIndexMap().get(f.getVertex1()) + 1; + int index2 = getVertexIndexMap().get(f.getVertex2()) + 1; + int index3 = getVertexIndexMap().get(f.getVertex3()) + 1; + pw.printf("f %d %d %d\n", index1, index2, index3); + } + + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage()); + } + } + + public static class Builder { + private List vertexList; + private List facetIndexList; + private List facetList; + + private Map vertexIndexMap; + private Map facetIndexMap; + + private Map> vertexFacetMap; + + public Builder() { + this.vertexList = new ArrayList<>(); + this.facetIndexList = new ArrayList<>(); + this.facetList = new ArrayList<>(); + + this.vertexIndexMap = new HashMap<>(); + this.facetIndexMap = new HashMap<>(); + this.vertexFacetMap = new HashMap<>(); + } + + /** + * Add a vertex to the mesh. + * + * @param vertex + */ + public void addVertex(UnwritableVectorIJK vertex) { + vertexIndexMap.put(vertex, vertexList.size()); + vertexList.add(vertex); + } + + /** + * Add a facet to the mesh. + * + * @param index1 index of vertex 1 + * @param index2 index of vertex 2 + * @param index3 index of vertex 3 + */ + public void addFacet(int index1, int index2, int index3) { + facetIndexList.add(new UnwritableVectorIJK(index1, index2, index3)); + List vertices = new ArrayList<>(); + vertices.add(vertexList.get(index1)); + vertices.add(vertexList.get(index2)); + vertices.add(vertexList.get(index3)); + + TriangularFacet tf = new TriangularFacet(vertices.get(0), vertices.get(1), vertices.get(2)); + facetIndexMap.put(tf, facetList.size()); + facetList.add(tf); + + for (UnwritableVectorIJK vertex : vertices) { + List facets = vertexFacetMap.get(vertex); + if (facets == null) { + facets = new ArrayList<>(); + vertexFacetMap.put(vertex, facets); + } + facets.add(tf); + } + } + + /** + * Return the mesh + * + * @return + */ + public TriangularMesh build() { + TriangularMesh mesh = new TriangularMesh(); + mesh.vertexList = Collections.unmodifiableList(vertexList); + mesh.facetList = Collections.unmodifiableList(facetList); + + Statistics.Builder builder = Statistics.builder(); + for (TriangularFacet f : facetList) { + builder.accumulate(f.getMeanEdgeLength()); + } + Statistics edgeStats = builder.build(); + + mesh.vertexIndexMap = Collections.unmodifiableMap(vertexIndexMap); + mesh.facetIndexMap = Collections.unmodifiableMap(facetIndexMap); + + mesh.vertexFacetMap = Collections.unmodifiableMap(vertexFacetMap); + + Statistics.Builder xBuilder = Statistics.builder(); + Statistics.Builder yBuilder = Statistics.builder(); + Statistics.Builder zBuilder = Statistics.builder(); + + for (UnwritableVectorIJK v : vertexList) { + xBuilder.accumulate(v.getI()); + yBuilder.accumulate(v.getJ()); + zBuilder.accumulate(v.getK()); + } + Statistics xStats = xBuilder.build(); + Statistics yStats = yBuilder.build(); + Statistics zStats = zBuilder.build(); + + mesh.center = new UnwritableVectorIJK(xStats.getMean(), yStats.getMean(), zStats.getMean()); + mesh.boundingBox = new BoundingBox( + new UnwritableVectorIJK( + xStats.getMinimumValue(), yStats.getMinimumValue(), zStats.getMinimumValue()), + new UnwritableVectorIJK( + xStats.getMaximumValue(), yStats.getMaximumValue(), zStats.getMaximumValue())); + mesh.threadLocalOctree = new ThreadLocal<>(); + + // this ratio should be about 10 + double maxSide = Math.max( + mesh.boundingBox.getxRange().getLength(), + Math.max( + mesh.boundingBox.getyRange().getLength(), + mesh.boundingBox.getzRange().getLength())); + + // octree levels above 10 require box indices to be long + int octreeLevel = + Math.min(10, (int) (Math.log(maxSide / (10 * edgeStats.getMean())) / Math.log(2.0) + 0.5)); + double boxSize = maxSide / Math.pow(2, octreeLevel); + logger.printf( + Level.DEBUG, + "Octree level %d, Mean edge length %f, box size %f, box size/edge length %f", + octreeLevel, + edgeStats.getMean(), + boxSize, + boxSize / edgeStats.getMean()); + mesh.buildFacetOctreeMap(octreeLevel); + return mesh; + } + } + + private Octree getOctree() { + Octree octree = threadLocalOctree.get(); + if (octree == null) { + octree = new Octree(boundingBox); + threadLocalOctree.set(octree); + } + return octree; + } + + /** + * Return a list of vertices in the order they were added. + * + * @return + */ + public List getVertexList() { + return Collections.unmodifiableList(vertexList); + } + + /** + * return a list of facets in the order they were added + * + * @return + */ + public List getFacetList() { + return Collections.unmodifiableList(facetList); + } + + /** + * Return a mapping from vertex to list index. Add 1 to this index to get the corresponding OBJ + * vertex number (which start from 1). + * + * @return + */ + public Map getVertexIndexMap() { + return vertexIndexMap; + } + + /** + * return a mapping from facet to list index + * + * @return + */ + public Map getFacetIndexMap() { + return facetIndexMap; + } + + /** + * Return a list of facets containing this vertex + * + * @return + */ + public Map> getVertexFacetMap() { + return vertexFacetMap; + } + + /** + * return the mean of all vertex positions + * + * @return + */ + public UnwritableVectorIJK getCenter() { + return center; + } + + /** + * return a mapping of octree box index to {@link Set} of facets contained in the box + * + * @return + */ + public Map> getFacetOctreeMap() { + return facetOctreeMap; + } + + /** + * Get the {@link BoundingBox} which encloses the mesh with its edges parallel to the coordinate + * system axes. + * + * @return + */ + public BoundingBox getBoundingBox() { + return boundingBox; + } + + private void buildFacetOctreeMap(int level) { + facetOctreeMap = new HashMap<>(); + octreeLevel = level; + for (TriangularFacet facet : facetList) { + for (UnwritableVectorIJK v : facet.getVertices()) { + int index = getOctree().getIndex(v, level); + Set facets = facetOctreeMap.get(index); + if (facets == null) { + facets = new HashSet<>(); + facetOctreeMap.put(index, facets); + } + facets.add(facet); + } + } + } + + /** + * Return a set of facets sharing a vertex with this one + * + * @return + */ + public Set getFacetNeighbors(TriangularFacet tf) { + Set facets = new TreeSet<>(FACET_DISTANCE_COMPARATOR(tf)); + + for (UnwritableVectorIJK vertex : tf.getVertices()) { + facets.addAll(vertexFacetMap.get(vertex)); + } + return Collections.unmodifiableSet(facets); + } + + /** + * Return a contiguous set of facets with centers less than radius from this facet's center + * + * @return + */ + public Set getFacetsWithinRadius(TriangularFacet tf, double radius) { + Set facets = new TreeSet<>(FACET_DISTANCE_COMPARATOR(tf)); + facets.add(tf); + + UnwritableVectorIJK center = tf.getCenter(); + + while (true) { + + Set currentFacets = new HashSet<>(facets); + int facetSize = currentFacets.size(); + + for (TriangularFacet facet : currentFacets) { + for (TriangularFacet neighbor : getFacetNeighbors(facet)) { + if (!facets.contains(neighbor)) { + if (neighbor.getCenter().getDistance(center) < radius) { + facets.add(neighbor); + } + } + } + } + + // we're done if we haven't added any more facets in this loop + if (facets.size() == facetSize) { + break; + } + } + + return Collections.unmodifiableSet(facets); + } + + /** + * Return a contiguous set of facets with centers within a specified radius of a point. The point + * does not have to be on the shape model. This probably won't work well for points that are + * inside the shape model. + * + * @param point + * @param radius + * @return + */ + public Set getFacetsWithinRadius(UnwritableVectorIJK point, double radius) { + + Set vertices = getVerticesWithinRadius(point, radius); + Set facets = new TreeSet<>(new Comparator() { + + @Override + public int compare(TriangularFacet o1, TriangularFacet o2) { + double dist1 = point.getDistance(o1.getCenter()); + double dist2 = point.getDistance(o2.getCenter()); + return Double.compare(dist1, dist2); + } }); - // Set candidates = new HashSet<>(); - for (int index : boxIndices) { - Set candidates = facetOctreeMap.get(index); - for (TriangularFacet f : candidates) { - if (f.intersects(origin, direction)) { - if (outsideBox || VectorIJK.subtract(f.getCenter(), origin).getDot(direction) > 0) { - intersectingFacets.add(f); - } + for (UnwritableVectorIJK v : vertices) { + List facetList = vertexFacetMap.get(v); + for (TriangularFacet facet : facetList) { + if (!facets.contains(facet)) { + if (facet.getCenter().getDistance(point) < radius) { + facets.add(facet); + } + } + } } - } - // candidates.addAll(facetOctreeMap.get(index)); + + return facets; } - // intersectingFacets.addAll(candidates.parallelStream().filter(f -> - // f.intersects(origin, direction)).collect(Collectors.toSet())); + /** + * Return all vertices of a contiguous set of facets within a specified radius of a point. The + * point does not have to be on the shape model. This probably won't work well for points that are + * inside the shape model. + * + * @param point + * @param radius + * @return + */ + public Set getVerticesWithinRadius(UnwritableVectorIJK point, double radius) { - return intersectingFacets; - } + Set vertices = new TreeSet<>(VERTEX_DISTANCE_COMPARATOR(point)); - /** - * Draw a ray from the Sun to the facet center. If the nearest facet intersected by the ray is - * this facet, it is not in shadow. - *

    - * TODO: use finite size of sun, allow for partial shadowing. - * - * @param f - * @param sunPos sun position in the mesh coordinate system - * @return 0 if not in shadow, 1 if shadowed - */ - public double isInShadow(TriangularFacet f, UnwritableVectorIJK sunPos) { - double shadow = 1; - VectorIJK lookDir = VectorIJK.subtract(f.getCenter(), sunPos); - if (f.getNormal().getDot(lookDir) < 0) { - NavigableSet list = getIntersections(sunPos, lookDir); - if (list.size() > 0 && f.equals(list.first())) { - shadow = 0.; - } - } - return shadow; - } + NavigableSet facets = getIntersections(getCenter(), VectorIJK.subtract(point, getCenter())); - @Override - public VectorIJK computeOutwardNormal(UnwritableVectorIJK surfacePoint, VectorIJK buffer) { - NavigableSet list = - getIntersections(getCenter(), VectorIJK.subtract(surfacePoint, getCenter())); - if (list.size() > 0) { - // note this is the closest facet to surfacePoint but may not be the last facet - // intersected by the ray on its way out - buffer.setTo(list.first().getNormal()); - } + while (true) { - return buffer; - } + Set currentFacets = new HashSet<>(facets); + int facetSize = currentFacets.size(); - @Override - public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { + for (TriangularFacet facet : currentFacets) { + for (TriangularFacet neighbor : getFacetNeighbors(facet)) { + for (UnwritableVectorIJK vertex : neighbor.getVertices()) { + if (!vertices.contains(vertex)) { + if (vertex.getDistance(point) < radius) { + vertices.add(vertex); + facets.add(neighbor); + } + } + } + } + } - // set of boxes intersected by the ray - Set boxIndices = getOctree().contains(0); - for (int level = 1; level < octreeLevel; level++) { - Set nextIndices = new HashSet<>(); - for (int index : boxIndices) { - if (getOctree().getBoundingBox(index).intersects(source, ray)) { - nextIndices.add(index); + // we're done if we haven't added any more facets in this loop + if (facets.size() == facetSize) { + break; + } } - } - boxIndices = new HashSet<>(); - for (int index : nextIndices) { - boxIndices.addAll(getOctree().contains(index)); - } + + return vertices; } - // intersection with set of boxes containing facets - boxIndices.retainAll(facetOctreeMap.keySet()); - for (int index : boxIndices) { - Set candidates = facetOctreeMap.get(index); - for (TriangularFacet f : candidates) { - if (f.intersects(source, ray)) { - return true; + /** + * Apply the rotation about the center of the shape model + * + * @param rotate + * @return + */ + public TriangularMesh rotate(UnwritableRotationMatrixIJK rotate) { + Builder builder = new Builder(); + UnwritableVectorIJK center = getCenter(); + for (UnwritableVectorIJK v : vertexList) { + VectorIJK rotatedVector = VectorIJK.add(rotate.mxv(VectorIJK.subtract(v, center)), center); + builder.addVertex(rotatedVector); } - } + + for (TriangularFacet f : facetList) { + builder.addFacet( + vertexList.indexOf(f.getVertex1()), + vertexList.indexOf(f.getVertex2()), + vertexList.indexOf(f.getVertex3())); + } + + return builder.build(); } - return false; - } + /** + * Return a new Triangular mesh scaled by the specified value + * + * @param scale + * @return + */ + public TriangularMesh scale(double scale) { - @Override - public VectorIJK compute(UnwritableVectorIJK source, UnwritableVectorIJK ray, VectorIJK buffer) { - NavigableSet list = getIntersections(source, ray); - if (list.size() > 0) { - buffer.setTo(list.first().getCenter()); + Builder builder = new Builder(); + UnwritableVectorIJK center = getCenter(); + for (UnwritableVectorIJK v : vertexList) { + VectorIJK scaledVector = VectorIJK.add(VectorIJK.subtract(v, center).scale(scale), center); + builder.addVertex(scaledVector); + } + + for (TriangularFacet f : facetList) { + builder.addFacet( + vertexList.indexOf(f.getVertex1()), + vertexList.indexOf(f.getVertex2()), + vertexList.indexOf(f.getVertex3())); + } + + return builder.build(); } - return buffer; - } + /** + * Create a mesh from the supplied facets + * + * @param facets + * @return + */ + public TriangularMesh subset(Collection facets) { + Set vertexSet = new HashSet<>(); + for (TriangularFacet facet : facets) { + vertexSet.addAll(facet.getVertices()); + } + + List vertexList = new ArrayList<>(); + vertexList.addAll(vertexSet); + + Builder builder = new Builder(); + for (UnwritableVectorIJK vertex : vertexList) { + builder.addVertex(vertex); + } + for (TriangularFacet facet : facets) { + builder.addFacet( + vertexList.indexOf(facet.getVertex1()), + vertexList.indexOf(facet.getVertex2()), + vertexList.indexOf(facet.getVertex3())); + } + + return builder.build(); + } + + /** + * Return a new Triangular mesh translated by the specified vector + * + * @param translate + * @return + */ + public TriangularMesh translate(UnwritableVectorIJK translate) { + + Builder builder = new Builder(); + for (UnwritableVectorIJK v : vertexList) { + VectorIJK translatedVector = VectorIJK.add(v, translate); + builder.addVertex(translatedVector); + } + + for (TriangularFacet f : facetList) { + builder.addFacet( + vertexList.indexOf(f.getVertex1()), + vertexList.indexOf(f.getVertex2()), + vertexList.indexOf(f.getVertex3())); + } + + return builder.build(); + } + + /** + * Calculate facets intersected by the ray with its vertex at origin and pointing towards + * direction. Facets are ordered by distance to origin. If the origin is on a facet, check that + * the first intersection is not the facet containing the origin. + * + * @param origin + * @param direction + * @return + */ + public NavigableSet getIntersections(UnwritableVectorIJK origin, UnwritableVectorIJK direction) { + + /*- + List list = facetList.parallelStream() + .filter(f -> lookDir.getDirection().getDot(VectorIJK.subtract(f.getCenter(), + getCenter())) < 0 + && f.intersects(lookDir)) + .collect(Collectors.toList()); + */ + + // if the origin is inside the bounding box, do a check to ensure that any + // intersections are along the specified direction + final boolean outsideBox = !getOctree().getBoundingBox(0).closedContains(origin); + + // find the list of bounding boxes intersected by the ray + Set boxIndices = new HashSet<>(); + boxIndices.add(0); + for (int level = 0; level < octreeLevel; level++) { + Set nextIndices = new HashSet<>(); + for (int index : boxIndices) { + if (getOctree().getBoundingBox(index).intersects(origin, direction)) { + nextIndices.add(index); + } + } + boxIndices = new HashSet<>(); + for (int index : nextIndices) { + boxIndices.addAll(getOctree().contains(index)); + } + } + // intersection with set of boxes containing facets + boxIndices.retainAll(facetOctreeMap.keySet()); + + // now collect all the facets intersected by the ray, sorted by distance to the + // origin + NavigableSet intersectingFacets = new TreeSet<>(new Comparator() { + @Override + public int compare(TriangularFacet o1, TriangularFacet o2) { + return Double.compare( + VectorIJK.subtract(o1.getCenter(), origin).getLength(), + VectorIJK.subtract(o2.getCenter(), origin).getLength()); + } + }); + + // Set candidates = new HashSet<>(); + for (int index : boxIndices) { + Set candidates = facetOctreeMap.get(index); + for (TriangularFacet f : candidates) { + if (f.intersects(origin, direction)) { + if (outsideBox || VectorIJK.subtract(f.getCenter(), origin).getDot(direction) > 0) { + intersectingFacets.add(f); + } + } + } + // candidates.addAll(facetOctreeMap.get(index)); + } + + // intersectingFacets.addAll(candidates.parallelStream().filter(f -> + // f.intersects(origin, direction)).collect(Collectors.toSet())); + + return intersectingFacets; + } + + /** + * Draw a ray from the Sun to the facet center. If the nearest facet intersected by the ray is + * this facet, it is not in shadow. + *

    + * TODO: use finite size of sun, allow for partial shadowing. + * + * @param f + * @param sunPos sun position in the mesh coordinate system + * @return 0 if not in shadow, 1 if shadowed + */ + public double isInShadow(TriangularFacet f, UnwritableVectorIJK sunPos) { + double shadow = 1; + VectorIJK lookDir = VectorIJK.subtract(f.getCenter(), sunPos); + if (f.getNormal().getDot(lookDir) < 0) { + NavigableSet list = getIntersections(sunPos, lookDir); + if (list.size() > 0 && f.equals(list.first())) { + shadow = 0.; + } + } + return shadow; + } + + @Override + public VectorIJK computeOutwardNormal(UnwritableVectorIJK surfacePoint, VectorIJK buffer) { + NavigableSet list = + getIntersections(getCenter(), VectorIJK.subtract(surfacePoint, getCenter())); + if (list.size() > 0) { + // note this is the closest facet to surfacePoint but may not be the last facet + // intersected by the ray on its way out + buffer.setTo(list.first().getNormal()); + } + + return buffer; + } + + @Override + public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { + + // set of boxes intersected by the ray + Set boxIndices = getOctree().contains(0); + for (int level = 1; level < octreeLevel; level++) { + Set nextIndices = new HashSet<>(); + for (int index : boxIndices) { + if (getOctree().getBoundingBox(index).intersects(source, ray)) { + nextIndices.add(index); + } + } + boxIndices = new HashSet<>(); + for (int index : nextIndices) { + boxIndices.addAll(getOctree().contains(index)); + } + } + + // intersection with set of boxes containing facets + boxIndices.retainAll(facetOctreeMap.keySet()); + for (int index : boxIndices) { + Set candidates = facetOctreeMap.get(index); + for (TriangularFacet f : candidates) { + if (f.intersects(source, ray)) { + return true; + } + } + } + + return false; + } + + @Override + public VectorIJK compute(UnwritableVectorIJK source, UnwritableVectorIJK ray, VectorIJK buffer) { + NavigableSet list = getIntersections(source, ray); + if (list.size() > 0) { + buffer.setTo(list.first().getCenter()); + } + + return buffer; + } } diff --git a/src/main/java/terrasaur/utils/octree/BoundingBox.java b/src/main/java/terrasaur/utils/octree/BoundingBox.java index f430e8d..c9efddf 100644 --- a/src/main/java/terrasaur/utils/octree/BoundingBox.java +++ b/src/main/java/terrasaur/utils/octree/BoundingBox.java @@ -22,102 +22,98 @@ */ package terrasaur.utils.octree; - import picante.math.intervals.UnwritableInterval; import picante.math.vectorspace.UnwritableVectorIJK; /** * Define a 3D box using its minimum and maximum coordinates - * + * * @author nairah1 * */ public class BoundingBox { - private double[] minPt; - private double[] maxPt; - private UnwritableInterval xRange; - private UnwritableInterval yRange; - private UnwritableInterval zRange; + private double[] minPt; + private double[] maxPt; + private UnwritableInterval xRange; + private UnwritableInterval yRange; + private UnwritableInterval zRange; - public BoundingBox(UnwritableVectorIJK minPt, UnwritableVectorIJK maxPt) { - this.minPt = new double[] {minPt.getI(), minPt.getJ(), minPt.getK()}; - this.maxPt = new double[] {maxPt.getI(), maxPt.getJ(), maxPt.getK()}; + public BoundingBox(UnwritableVectorIJK minPt, UnwritableVectorIJK maxPt) { + this.minPt = new double[] {minPt.getI(), minPt.getJ(), minPt.getK()}; + this.maxPt = new double[] {maxPt.getI(), maxPt.getJ(), maxPt.getK()}; - this.xRange = new UnwritableInterval(this.minPt[0], this.maxPt[0]); - this.yRange = new UnwritableInterval(this.minPt[1], this.maxPt[1]); - this.zRange = new UnwritableInterval(this.minPt[2], this.maxPt[2]); - } - - public UnwritableVectorIJK minPt() { - return new UnwritableVectorIJK(minPt); - } - - public UnwritableVectorIJK maxPt() { - return new UnwritableVectorIJK(maxPt); - } - - public UnwritableInterval getxRange() { - return xRange; - } - - public UnwritableInterval getyRange() { - return yRange; - } - - public UnwritableInterval getzRange() { - return zRange; - } - - public boolean closedContains(UnwritableVectorIJK point) { - return (xRange.closedContains(point.getI()) && yRange.closedContains(point.getJ()) - && zRange.closedContains(point.getK())); - } - - /** - * - * Check if the ray intersects the bounding box. Algorithm is from - * https://medium.com/@bromanz/another-view-on-the-classic-ray-aabb-intersection-algorithm-for-bvh-traversal-41125138b525 - * - * @param source observer position - * @param ray look direction - * - * @return - */ - public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { - double[] origin = new double[] {source.getI(), source.getJ(), source.getK()}; - double[] invDir = new double[] {1. / ray.getI(), 1. / ray.getJ(), 1. / ray.getK()}; - double tmin = -Double.MAX_VALUE; - double tmax = Double.MAX_VALUE; - - for (int i = 0; i < 3; i++) { - double t0 = (minPt[i] - origin[i]) * invDir[i]; - double t1 = (maxPt[i] - origin[i]) * invDir[i]; - double tsmall = Math.min(t0, t1); - double tlarge = Math.max(t0, t1); - - tmin = Math.max(tmin, tsmall); - tmax = Math.min(tmax, tlarge); - - if (tmax < tmin) { - return false; - } + this.xRange = new UnwritableInterval(this.minPt[0], this.maxPt[0]); + this.yRange = new UnwritableInterval(this.minPt[1], this.maxPt[1]); + this.zRange = new UnwritableInterval(this.minPt[2], this.maxPt[2]); } - return true; - } + public UnwritableVectorIJK minPt() { + return new UnwritableVectorIJK(minPt); + } - @Override - public String toString() { - return String.format("%s - %s", new UnwritableVectorIJK(minPt), new UnwritableVectorIJK(maxPt)); - } + public UnwritableVectorIJK maxPt() { + return new UnwritableVectorIJK(maxPt); + } - public static void main(String[] args) { - BoundingBox bb = - new BoundingBox(new UnwritableVectorIJK(-1, -1, 1), new UnwritableVectorIJK(1, 1, 2)); - System.out.println( - bb.intersects(new UnwritableVectorIJK(0, 0, 0), new UnwritableVectorIJK(1, 1, 1.01))); + public UnwritableInterval getxRange() { + return xRange; + } - } + public UnwritableInterval getyRange() { + return yRange; + } + public UnwritableInterval getzRange() { + return zRange; + } + + public boolean closedContains(UnwritableVectorIJK point) { + return (xRange.closedContains(point.getI()) + && yRange.closedContains(point.getJ()) + && zRange.closedContains(point.getK())); + } + + /** + * + * Check if the ray intersects the bounding box. Algorithm is from + * https://medium.com/@bromanz/another-view-on-the-classic-ray-aabb-intersection-algorithm-for-bvh-traversal-41125138b525 + * + * @param source observer position + * @param ray look direction + * + * @return + */ + public boolean intersects(UnwritableVectorIJK source, UnwritableVectorIJK ray) { + double[] origin = new double[] {source.getI(), source.getJ(), source.getK()}; + double[] invDir = new double[] {1. / ray.getI(), 1. / ray.getJ(), 1. / ray.getK()}; + double tmin = -Double.MAX_VALUE; + double tmax = Double.MAX_VALUE; + + for (int i = 0; i < 3; i++) { + double t0 = (minPt[i] - origin[i]) * invDir[i]; + double t1 = (maxPt[i] - origin[i]) * invDir[i]; + double tsmall = Math.min(t0, t1); + double tlarge = Math.max(t0, t1); + + tmin = Math.max(tmin, tsmall); + tmax = Math.min(tmax, tlarge); + + if (tmax < tmin) { + return false; + } + } + + return true; + } + + @Override + public String toString() { + return String.format("%s - %s", new UnwritableVectorIJK(minPt), new UnwritableVectorIJK(maxPt)); + } + + public static void main(String[] args) { + BoundingBox bb = new BoundingBox(new UnwritableVectorIJK(-1, -1, 1), new UnwritableVectorIJK(1, 1, 2)); + System.out.println(bb.intersects(new UnwritableVectorIJK(0, 0, 0), new UnwritableVectorIJK(1, 1, 1.01))); + } } diff --git a/src/main/java/terrasaur/utils/octree/Octree.java b/src/main/java/terrasaur/utils/octree/Octree.java index 1c2e80a..6f281d7 100644 --- a/src/main/java/terrasaur/utils/octree/Octree.java +++ b/src/main/java/terrasaur/utils/octree/Octree.java @@ -22,14 +22,14 @@ */ package terrasaur.utils.octree; -import picante.math.vectorspace.UnwritableVectorIJK; -import picante.math.vectorspace.VectorIJK; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; +import picante.math.vectorspace.UnwritableVectorIJK; +import picante.math.vectorspace.VectorIJK; /** * Implementation of a 3D heirarchical octree. The level 0 octree is set to the user supplied @@ -110,198 +110,196 @@ import java.util.TreeMap; * 0.001953125 * * - * + * * This class supports up to 10 levels in the hierarchy. More boxes will overflow a 32 bit int. - * + * * @author nairah1 * */ public class Octree { - /** - * need to use longs for index if a higher MAX_LEVEL is desired - */ - public static final int MAX_LEVEL = 10; + /** + * need to use longs for index if a higher MAX_LEVEL is desired + */ + public static final int MAX_LEVEL = 10; - private BoundingBox boundingBox; - private Map startIndexMap; - private NavigableMap indexToLevelMap; - private Map> containsMap = new HashMap<>(); + private BoundingBox boundingBox; + private Map startIndexMap; + private NavigableMap indexToLevelMap; + private Map> containsMap = new HashMap<>(); - /** - * Create an Octree from this BoundingBox - * - * @param boundingBox - */ - public Octree(BoundingBox boundingBox) { - this.boundingBox = boundingBox; + /** + * Create an Octree from this BoundingBox + * + * @param boundingBox + */ + public Octree(BoundingBox boundingBox) { + this.boundingBox = boundingBox; - indexToLevelMap = new TreeMap<>(); - startIndexMap = new HashMap<>(); - startIndexMap.put(0, 0); - indexToLevelMap.put(0, 0); - for (int i = 1; i < MAX_LEVEL + 1; i++) { - startIndexMap.put(i, getMinIndex(i)); - indexToLevelMap.put(getMaxIndex(i), i); - } - } - - private int getMinIndex(int level) { - if (level < 1) { - return 0; - } - return getMinIndex(level - 1) + (int) Math.pow(8, level - 1); - } - - private int getMaxIndex(int level) { - return getMinIndex(level) + (int) Math.pow(8, level) - 1; - } - - /** - * Return a set of indices contained by the bounding box defined by index - * - * @param index - * @return - */ - public Set contains(int index) { - Set indices = new HashSet<>(); - - if (containsMap.containsKey(index)) { - indices = containsMap.get(index); - } else { - int level = getLevel(index); - int startIndex = startIndexMap.get(level); - startIndexMap.put(level + 1, getMinIndex(level + 1)); - indexToLevelMap.put(getMaxIndex(level + 1), level + 1); - - int side = (int) Math.pow(2, level); - int iindex = index - startIndex; - - int iz = iindex / side / side; - int iy = (iindex - iz * side * side) / side; - int ix = iindex - side * (iy + iz * side); - - side *= 2; - startIndex = startIndexMap.get(level + 1); - - ix *= 2; - iy *= 2; - iz *= 2; - - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - for (int k = 0; k < 2; k++) { - indices.add((ix + i) + side * (iy + j + side * (iz + k)) + startIndex); - } + indexToLevelMap = new TreeMap<>(); + startIndexMap = new HashMap<>(); + startIndexMap.put(0, 0); + indexToLevelMap.put(0, 0); + for (int i = 1; i < MAX_LEVEL + 1; i++) { + startIndexMap.put(i, getMinIndex(i)); + indexToLevelMap.put(getMaxIndex(i), i); } - } - containsMap.put(index, indices); - } - return indices; - } - - /** - * Given the index of a box, find its level in the Octree - * - * @param index - * @return - */ - public int getLevel(int index) { - return indexToLevelMap.get(indexToLevelMap.ceilingKey(index)); - } - - /** - * Given a 3D point and level return the index of the smallest box containing the point - * - * @param point - * @param level - * @return - */ - public int getIndex(UnwritableVectorIJK point, int level) { - UnwritableVectorIJK minPt = boundingBox.minPt(); - UnwritableVectorIJK maxPt = boundingBox.maxPt(); - - double xScale = maxPt.getI() - minPt.getI(); - double yScale = maxPt.getJ() - minPt.getJ(); - double zScale = maxPt.getK() - minPt.getK(); - double x = (point.getI() - minPt.getI()) / xScale; - double y = (point.getJ() - minPt.getJ()) / yScale; - double z = (point.getK() - minPt.getK()) / zScale; - - int startIndex = startIndexMap.get(level); - int side = (int) Math.pow(2, level); - - int ix = (int) (x * side); - int iy = (int) (y * side); - int iz = (int) (z * side); - - return ix + side * (iy + side * iz) + startIndex; - } - - /** - * Return the bounding box corresponding to index - * - * @param index - * @return - */ - public BoundingBox getBoundingBox(int index) { - int level = getLevel(index); - long startIndex = startIndexMap.get(level); - - int side = (int) Math.pow(2, level); - long iindex = index - startIndex; - - long iz = iindex / side / side; - long iy = iindex / side - iz * side; - long ix = iindex - side * (iy + iz * side); - - double minX = ((double) ix) / side; - double maxX = (ix + 1.) / side; - double minY = ((double) iy) / side; - double maxY = (iy + 1.) / side; - double minZ = ((double) iz) / side; - double maxZ = (iz + 1.) / side; - - UnwritableVectorIJK minPt = boundingBox.minPt(); - UnwritableVectorIJK maxPt = boundingBox.maxPt(); - double xScale = maxPt.getI() - minPt.getI(); - minX = xScale * minX + minPt.getI(); - maxX = xScale * maxX + minPt.getI(); - double yScale = maxPt.getJ() - minPt.getJ(); - minY = yScale * minY + minPt.getJ(); - maxY = yScale * maxY + minPt.getJ(); - double zScale = maxPt.getK() - minPt.getK(); - minZ = zScale * minZ + minPt.getK(); - maxZ = zScale * maxZ + minPt.getK(); - - return new BoundingBox(new UnwritableVectorIJK(minX, minY, minZ), - new UnwritableVectorIJK(maxX, maxY, maxZ)); - } - - public static void main(String[] args) { - int index = 0; - BoundingBox bb = new BoundingBox(VectorIJK.ZERO, new UnwritableVectorIJK(1, 1, 1)); - Octree o = new Octree(bb); - Set indices = o.contains(index); - BoundingBox orig = o.getBoundingBox(index); - System.out.println(orig); - for (int i : indices) { - System.out.println(o.getBoundingBox(i)); } - UnwritableVectorIJK point = new UnwritableVectorIJK(0.1, 6.2, 0.77); - for (int level = 0; level < Octree.MAX_LEVEL; level++) { - index = o.getIndex(point, level); - System.out.printf("%d %d %s\n", level, index, o.getBoundingBox(index)); + private int getMinIndex(int level) { + if (level < 1) { + return 0; + } + return getMinIndex(level - 1) + (int) Math.pow(8, level - 1); } - /*- - for (int level = 0; level < Octree.MAX_LEVEL; level++) { - index = o.getIndex(point, level); - BoundingBox first = o.getBoundingBox(o.getMinIndex(level)); - System.out.printf("%d%9d%9d%.9f\n", level, o.getMinIndex(level), o.getMaxIndex(level), - first.getxRange().getLength()); - } - */ - } + private int getMaxIndex(int level) { + return getMinIndex(level) + (int) Math.pow(8, level) - 1; + } + + /** + * Return a set of indices contained by the bounding box defined by index + * + * @param index + * @return + */ + public Set contains(int index) { + Set indices = new HashSet<>(); + + if (containsMap.containsKey(index)) { + indices = containsMap.get(index); + } else { + int level = getLevel(index); + int startIndex = startIndexMap.get(level); + startIndexMap.put(level + 1, getMinIndex(level + 1)); + indexToLevelMap.put(getMaxIndex(level + 1), level + 1); + + int side = (int) Math.pow(2, level); + int iindex = index - startIndex; + + int iz = iindex / side / side; + int iy = (iindex - iz * side * side) / side; + int ix = iindex - side * (iy + iz * side); + + side *= 2; + startIndex = startIndexMap.get(level + 1); + + ix *= 2; + iy *= 2; + iz *= 2; + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + for (int k = 0; k < 2; k++) { + indices.add((ix + i) + side * (iy + j + side * (iz + k)) + startIndex); + } + } + } + containsMap.put(index, indices); + } + return indices; + } + + /** + * Given the index of a box, find its level in the Octree + * + * @param index + * @return + */ + public int getLevel(int index) { + return indexToLevelMap.get(indexToLevelMap.ceilingKey(index)); + } + + /** + * Given a 3D point and level return the index of the smallest box containing the point + * + * @param point + * @param level + * @return + */ + public int getIndex(UnwritableVectorIJK point, int level) { + UnwritableVectorIJK minPt = boundingBox.minPt(); + UnwritableVectorIJK maxPt = boundingBox.maxPt(); + + double xScale = maxPt.getI() - minPt.getI(); + double yScale = maxPt.getJ() - minPt.getJ(); + double zScale = maxPt.getK() - minPt.getK(); + double x = (point.getI() - minPt.getI()) / xScale; + double y = (point.getJ() - minPt.getJ()) / yScale; + double z = (point.getK() - minPt.getK()) / zScale; + + int startIndex = startIndexMap.get(level); + int side = (int) Math.pow(2, level); + + int ix = (int) (x * side); + int iy = (int) (y * side); + int iz = (int) (z * side); + + return ix + side * (iy + side * iz) + startIndex; + } + + /** + * Return the bounding box corresponding to index + * + * @param index + * @return + */ + public BoundingBox getBoundingBox(int index) { + int level = getLevel(index); + long startIndex = startIndexMap.get(level); + + int side = (int) Math.pow(2, level); + long iindex = index - startIndex; + + long iz = iindex / side / side; + long iy = iindex / side - iz * side; + long ix = iindex - side * (iy + iz * side); + + double minX = ((double) ix) / side; + double maxX = (ix + 1.) / side; + double minY = ((double) iy) / side; + double maxY = (iy + 1.) / side; + double minZ = ((double) iz) / side; + double maxZ = (iz + 1.) / side; + + UnwritableVectorIJK minPt = boundingBox.minPt(); + UnwritableVectorIJK maxPt = boundingBox.maxPt(); + double xScale = maxPt.getI() - minPt.getI(); + minX = xScale * minX + minPt.getI(); + maxX = xScale * maxX + minPt.getI(); + double yScale = maxPt.getJ() - minPt.getJ(); + minY = yScale * minY + minPt.getJ(); + maxY = yScale * maxY + minPt.getJ(); + double zScale = maxPt.getK() - minPt.getK(); + minZ = zScale * minZ + minPt.getK(); + maxZ = zScale * maxZ + minPt.getK(); + + return new BoundingBox(new UnwritableVectorIJK(minX, minY, minZ), new UnwritableVectorIJK(maxX, maxY, maxZ)); + } + + public static void main(String[] args) { + int index = 0; + BoundingBox bb = new BoundingBox(VectorIJK.ZERO, new UnwritableVectorIJK(1, 1, 1)); + Octree o = new Octree(bb); + Set indices = o.contains(index); + BoundingBox orig = o.getBoundingBox(index); + System.out.println(orig); + for (int i : indices) { + System.out.println(o.getBoundingBox(i)); + } + + UnwritableVectorIJK point = new UnwritableVectorIJK(0.1, 6.2, 0.77); + for (int level = 0; level < Octree.MAX_LEVEL; level++) { + index = o.getIndex(point, level); + System.out.printf("%d %d %s\n", level, index, o.getBoundingBox(index)); + } + /*- + for (int level = 0; level < Octree.MAX_LEVEL; level++) { + index = o.getIndex(point, level); + BoundingBox first = o.getBoundingBox(o.getMinIndex(level)); + System.out.printf("%d%9d%9d%.9f\n", level, o.getMinIndex(level), o.getMaxIndex(level), + first.getxRange().getLength()); + } + */ + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/ActivityPlot.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/ActivityPlot.java index 5ab0a38..9f1aa74 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/ActivityPlot.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/ActivityPlot.java @@ -39,109 +39,96 @@ import terrasaur.utils.saaPlotLib.util.Keyword; public class ActivityPlot extends RectangularPlotCanvas { - boolean outline; + boolean outline; - public ActivityPlot(PlotConfig config) { - super(config); - outline = true; - } - - public ActivityPlot setOutline(boolean outline) { - this.outline = outline; - return this; - } - - /** draw the axes - note tick labels are suppressed if axis title length is 0. */ - @Override - public void drawAxes() { - // draw the y axes in plot(ActivitySet, Axis) - drawLowerAxis(); - drawUpperAxis(); - - if (!config.title().isEmpty()) { - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - - int x = config.leftMargin() + config.width() / 2; - - g.setFont(config.titleFont()); - g.setColor(Color.BLACK); - addAnnotation( - g, - config.title(), - x, - config.topMargin() * 0.2, - Keyword.ALIGN_CENTER, - Keyword.ALIGN_CENTER); + public ActivityPlot(PlotConfig config) { + super(config); + outline = true; } - } - public void plot(List activityList) { - double rotateLabels = yLeftAxis.getRotateLabels(); - double rotateTitle = yLeftAxis.getRotateTitle(); + public ActivityPlot setOutline(boolean outline) { + this.outline = outline; + return this; + } - NavigableSet minorTicks = yLeftAxis.getMinorTicks(); - yLeftAxis = new AxisY(-0.5, activityList.size() - 0.5, yLeftAxis.getTitle()); - yLeftAxis.setMinorTicks(minorTicks); - yLeftAxis.setRotateLabels(rotateLabels); - yLeftAxis.setRotateTitle(rotateTitle); + /** draw the axes - note tick labels are suppressed if axis title length is 0. */ + @Override + public void drawAxes() { + // draw the y axes in plot(ActivitySet, Axis) + drawLowerAxis(); + drawUpperAxis(); - yRightAxis = new AxisY(yLeftAxis.getRange().getBegin(), yLeftAxis.getRange().getEnd(), ""); - yRightAxis.setMinorTicks(minorTicks); + if (!config.title().isEmpty()) { + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); - NavigableMap labels = new TreeMap<>(); - NavigableMap emptyLabels = new TreeMap<>(); - double barHalfHeight = 0.25; + int x = config.leftMargin() + config.width() / 2; - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); - - for (int i = 0; i < activityList.size(); i++) { - ActivitySet as = activityList.get(activityList.size() - 1 - i); - labels.put((double) i, String.format("%s", as.getName())); - emptyLabels.put((double) i, ""); - - Map activityMap = as.getActivityMap(); - for (Interval interval : activityMap.keySet()) { - Activity a = activityMap.get(interval); - - Color fillColor = a.color(); - g.setColor(fillColor); - - double bottomY = dataYtoPixel(yLeftAxis, i + barHalfHeight); - double height = dataYtoPixel(yLeftAxis, i - barHalfHeight) - bottomY; - - double leftX = dataXtoPixel(xLowerAxis, interval.getInf()); - double rightX = dataXtoPixel(xLowerAxis, interval.getSup()); - - if (a.symbol().isPresent()) { - Symbol symbol = a.symbol().get(); - double y = dataYtoPixel(yLeftAxis, i); - symbol.draw(g, leftX, y); - symbol.draw(g, rightX, y); - } else { - double width = rightX - leftX; - g.fillRect( - (int) (leftX + 0.5), - (int) (bottomY + 0.5), - (int) (width + 0.5), - (int) (height + 0.5)); - g.setColor(outline ? Color.BLACK : fillColor); - g.drawRect( - (int) (leftX + 0.5), - (int) (bottomY + 0.5), - (int) (width + 0.5), - (int) (height + 0.5)); + g.setFont(config.titleFont()); + g.setColor(Color.BLACK); + addAnnotation(g, config.title(), x, config.topMargin() * 0.2, Keyword.ALIGN_CENTER, Keyword.ALIGN_CENTER); } - g.setColor(fillColor); - } } - yLeftAxis.setTickLabels(labels); - yRightAxis.setTickLabels(emptyLabels); + public void plot(List activityList) { + double rotateLabels = yLeftAxis.getRotateLabels(); + double rotateTitle = yLeftAxis.getRotateTitle(); - drawLeftAxis(); - drawRightAxis(); - } + NavigableSet minorTicks = yLeftAxis.getMinorTicks(); + yLeftAxis = new AxisY(-0.5, activityList.size() - 0.5, yLeftAxis.getTitle()); + yLeftAxis.setMinorTicks(minorTicks); + yLeftAxis.setRotateLabels(rotateLabels); + yLeftAxis.setRotateTitle(rotateTitle); + + yRightAxis = + new AxisY(yLeftAxis.getRange().getBegin(), yLeftAxis.getRange().getEnd(), ""); + yRightAxis.setMinorTicks(minorTicks); + + NavigableMap labels = new TreeMap<>(); + NavigableMap emptyLabels = new TreeMap<>(); + double barHalfHeight = 0.25; + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); + + for (int i = 0; i < activityList.size(); i++) { + ActivitySet as = activityList.get(activityList.size() - 1 - i); + labels.put((double) i, String.format("%s", as.getName())); + emptyLabels.put((double) i, ""); + + Map activityMap = as.getActivityMap(); + for (Interval interval : activityMap.keySet()) { + Activity a = activityMap.get(interval); + + Color fillColor = a.color(); + g.setColor(fillColor); + + double bottomY = dataYtoPixel(yLeftAxis, i + barHalfHeight); + double height = dataYtoPixel(yLeftAxis, i - barHalfHeight) - bottomY; + + double leftX = dataXtoPixel(xLowerAxis, interval.getInf()); + double rightX = dataXtoPixel(xLowerAxis, interval.getSup()); + + if (a.symbol().isPresent()) { + Symbol symbol = a.symbol().get(); + double y = dataYtoPixel(yLeftAxis, i); + symbol.draw(g, leftX, y); + symbol.draw(g, rightX, y); + } else { + double width = rightX - leftX; + g.fillRect((int) (leftX + 0.5), (int) (bottomY + 0.5), (int) (width + 0.5), (int) (height + 0.5)); + g.setColor(outline ? Color.BLACK : fillColor); + g.drawRect((int) (leftX + 0.5), (int) (bottomY + 0.5), (int) (width + 0.5), (int) (height + 0.5)); + } + g.setColor(fillColor); + } + } + + yLeftAxis.setTickLabels(labels); + yRightAxis.setTickLabels(emptyLabels); + + drawLeftAxis(); + drawRightAxis(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/AreaPlot.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/AreaPlot.java index be19f57..0e507b3 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/AreaPlot.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/AreaPlot.java @@ -36,101 +36,91 @@ import terrasaur.utils.saaPlotLib.config.PlotConfig; public class AreaPlot extends DiscreteDataPlot { - public AreaPlot(PlotConfig config) { - super(config); - } + public AreaPlot(PlotConfig config) { + super(config); + } - /** - * Plot the 2D function using the ramp and supplied axes. - * - * @param func 2D function to plot - * @param ramp color ramp - * @param xAxis X axis - * @param yAxis Y axis - */ - public void plot(MultivariateFunction func, ColorRamp ramp, AxisX xAxis, AxisY yAxis) { - plot(func, ramp, xAxis, yAxis, xAxis.getRange(), yAxis.getRange()); - } + /** + * Plot the 2D function using the ramp and supplied axes. + * + * @param func 2D function to plot + * @param ramp color ramp + * @param xAxis X axis + * @param yAxis Y axis + */ + public void plot(MultivariateFunction func, ColorRamp ramp, AxisX xAxis, AxisY yAxis) { + plot(func, ramp, xAxis, yAxis, xAxis.getRange(), yAxis.getRange()); + } - /** - * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied ranges - * will not be plotted. - * - * @param func 2D function to plot - * @param ramp color ramp - * @param xAxis X axis - * @param yAxis Y axis - * @param xRange X axis range - * @param yRange Y axis range - */ - public void plot( - MultivariateFunction func, - ColorRamp ramp, - AxisX xAxis, - AxisY yAxis, - AxisRange xRange, - AxisRange yRange) { - double[] point = new double[2]; - for (int i = 0; i < config.width(); i++) { - point[0] = pixelXtoData(xAxis, config.leftMargin() + i); - if (xRange.closedContains(point[0])) { - for (int j = 0; j < config.height(); j++) { - point[1] = pixelYtoData(yAxis, config.topMargin() + j); - if (yRange.closedContains(point[1])) { - double value = func.value(point); - Color color = ramp.getColor(value); - if (color.getAlpha() > 0) - image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); - } + /** + * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied ranges + * will not be plotted. + * + * @param func 2D function to plot + * @param ramp color ramp + * @param xAxis X axis + * @param yAxis Y axis + * @param xRange X axis range + * @param yRange Y axis range + */ + public void plot( + MultivariateFunction func, ColorRamp ramp, AxisX xAxis, AxisY yAxis, AxisRange xRange, AxisRange yRange) { + double[] point = new double[2]; + for (int i = 0; i < config.width(); i++) { + point[0] = pixelXtoData(xAxis, config.leftMargin() + i); + if (xRange.closedContains(point[0])) { + for (int j = 0; j < config.height(); j++) { + point[1] = pixelYtoData(yAxis, config.topMargin() + j); + if (yRange.closedContains(point[1])) { + double value = func.value(point); + Color color = ramp.getColor(value); + if (color.getAlpha() > 0) + image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); + } + } + } } - } } - } - /** - * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied shape - * will not be plotted. - * - * @param func 2D function to plot - * @param ramp color ramp - * @param xAxis X axis - * @param yAxis Y axis - * @param path shape in data coordinates - */ - public void plot( - MultivariateFunction func, - ColorRamp ramp, - AxisX xAxis, - AxisY yAxis, - List path) { + /** + * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied shape + * will not be plotted. + * + * @param func 2D function to plot + * @param ramp color ramp + * @param xAxis X axis + * @param yAxis Y axis + * @param path shape in data coordinates + */ + public void plot(MultivariateFunction func, ColorRamp ramp, AxisX xAxis, AxisY yAxis, List path) { - // create a shape in pixel coordinates - GeneralPath gp = new GeneralPath(); - double x = dataXtoPixel(xAxis, path.get(0).getX()); - double y = dataYtoPixel(yAxis, path.get(0).getY()); - gp.moveTo(x, y); - for (int i = 1; i < path.size(); i++) { - Point2D.Double xy = path.get(i); - x = dataXtoPixel(xAxis, xy.getX()); - y = dataYtoPixel(yAxis, xy.getY()); - gp.lineTo(x, y); - } - gp.closePath(); - - Rectangle2D bounds = gp.getBounds(); - - double[] point = new double[2]; - for (int i = (int) bounds.getMinX(); i < (int) bounds.getMaxX() + 1; i++) { - for (int j = (int) bounds.getMinY(); j < (int) bounds.getMaxY() + 1; j++) { - if (gp.contains(i, j)) { - point[0] = pixelXtoData(xAxis, config.leftMargin() + i); - point[1] = pixelYtoData(yAxis, config.topMargin() + j); - double value = func.value(point); - Color color = ramp.getColor(value); - if (color.getAlpha() > 0) - image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); + // create a shape in pixel coordinates + GeneralPath gp = new GeneralPath(); + double x = dataXtoPixel(xAxis, path.get(0).getX()); + double y = dataYtoPixel(yAxis, path.get(0).getY()); + gp.moveTo(x, y); + for (int i = 1; i < path.size(); i++) { + Point2D.Double xy = path.get(i); + x = dataXtoPixel(xAxis, xy.getX()); + y = dataYtoPixel(yAxis, xy.getY()); + gp.lineTo(x, y); + } + gp.closePath(); + + Rectangle2D bounds = gp.getBounds(); + + double[] point = new double[2]; + for (int i = (int) bounds.getMinX(); i < (int) bounds.getMaxX() + 1; i++) { + for (int j = (int) bounds.getMinY(); j < (int) bounds.getMaxY() + 1; j++) { + if (gp.contains(i, j)) { + point[0] = pixelXtoData(xAxis, config.leftMargin() + i); + point[1] = pixelYtoData(yAxis, config.topMargin() + j); + double value = func.value(point); + Color color = ramp.getColor(value); + if (color.getAlpha() > 0) + image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); + } + } } - } } - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlot.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlot.java index 8067533..e04c555 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlot.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlot.java @@ -39,362 +39,336 @@ import terrasaur.utils.saaPlotLib.data.PointList; public class DiscreteDataPlot extends RectangularPlotCanvas { - // used for sand plot - private PointList baseline; + // used for sand plot + private PointList baseline; - public DiscreteDataPlot(PlotConfig config) { - super(config); - } - - /** - * Plots the DataSet ds using the X lower axis and Y left axis - * - * @param ds DiscreteDataSet to plot - */ - public void plot(DiscreteDataSet ds) { - plot(ds, xLowerAxis, yLeftAxis); - } - - /** - * Plots the DataSet ds using the supplied X axis and the Y left axis - * - * @param ds DiscreteDataSet to plot - * @param xAxis lower X axis - */ - public void plot(DiscreteDataSet ds, AxisX xAxis) { - plot(ds, xAxis, yLeftAxis); - } - - /** - * Plots the DataSet ds using the lower X axis and the supplied Y axis - * - * @param ds DiscreteDataSet to plot - * @param yAxis left Y axis - */ - public void plot(DiscreteDataSet ds, AxisY yAxis) { - plot(ds, xLowerAxis, yAxis); - } - - /** - * Plots the DataSet ds using the supplied axes - * - * @param ds DiscreteDataSet to plot - * @param xAxis lower X axis - * @param yAxis left Y axis - */ - public void plot(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { - switch (ds.getPlotType()) { - case LINE: - plotLine(ds, xAxis, yAxis); - break; - case BAR: - plotBar(ds, xAxis); - break; - case SAND: - plotSand(ds, xAxis, yAxis); - break; - case SYMBOL: - plotSymbol(ds, xAxis, yAxis); - break; - default: - } - } - - /** - * Draw a filled shape on the plot - * - * @param color fill color - * @param outline outline to fill - */ - public void plot(Color color, List outline) { - plot(color, outline, true); - } - - /** - * Draw a shape on the plot - * - * @param color shape color - * @param outline outline to draw - * @param fill if true, fill outline - */ - public void plot(Color color, List outline, boolean fill) { - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(color); - g.setClip( - new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); - - GeneralPath gp = new GeneralPath(); - Point2D xy; - for (Point2D point2D : outline) { - xy = point2D; - if (xy == null) continue; - - double pixelX = - xLowerAxis.dataToPixel(config.getLeftPlotEdge(), config.getRightPlotEdge(), xy.getX()); - double pixelY = - yLeftAxis.dataToPixel(config.getBottomPlotEdge(), config.getTopPlotEdge(), xy.getY()); - - if (gp.getCurrentPoint() == null) gp.moveTo(pixelX, pixelY); - else gp.lineTo(pixelX, pixelY); + public DiscreteDataPlot(PlotConfig config) { + super(config); } - if (gp.getCurrentPoint() != null) { - gp.closePath(); - if (fill) g.fill(gp); - g.draw(gp); - } - } - - private double roundToNearestTenth(double x) { - return Math.round(x * 10) / 10.; - } - - private void plotLine(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { - PointList xy = ds.getData(); - if (xy.size() == 0) return; - - Point4D prior = xy.getFirst(); - - // Store points to plot if they differ from the previous point by more than a tenth of a pixel. - // This can save a lot of memory if there are a lot of points. - List pointMap = new ArrayList<>(); - Point2D lastPoint = - new Point2D.Double( - roundToNearestTenth(dataXtoPixel(xAxis, prior.getX())), - roundToNearestTenth(dataYtoPixel(yAxis, prior.getY()))); - pointMap.add(lastPoint); - for (int i = 1; i < xy.size(); i++) { - Point4D p = xy.get(i); - Point2D nextPoint = - new Point2D.Double( - roundToNearestTenth(dataXtoPixel(xAxis, p.getX())), - roundToNearestTenth(dataYtoPixel(yAxis, p.getY()))); - if (lastPoint.getX() != nextPoint.getX() || lastPoint.getY() != nextPoint.getY()) { - pointMap.add(nextPoint); - lastPoint = nextPoint; - } + /** + * Plots the DataSet ds using the X lower axis and Y left axis + * + * @param ds DiscreteDataSet to plot + */ + public void plot(DiscreteDataSet ds) { + plot(ds, xLowerAxis, yLeftAxis); } - GeneralPath gp = new GeneralPath(); - gp.moveTo(pointMap.get(0).getX(), pointMap.get(0).getY()); - for (int i = 1; i < pointMap.size(); i++) { - gp.lineTo(pointMap.get(i).getX(), pointMap.get(i).getY()); + /** + * Plots the DataSet ds using the supplied X axis and the Y left axis + * + * @param ds DiscreteDataSet to plot + * @param xAxis lower X axis + */ + public void plot(DiscreteDataSet ds, AxisX xAxis) { + plot(ds, xAxis, yLeftAxis); } - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setClip( - new Rectangle2D.Double( - config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - g.draw(gp); - } - - private void plotBar(DiscreteDataSet ds, AxisX xAxis) { - PointList xy = ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); - if (xy.size() == 0) return; - - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setClip( - new Rectangle2D.Double( - config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); - - List xValues = xy.getX(); - // need a prior and next for each point - if (xValues.size() < 3) return; - - for (int i = 0; i < xValues.size(); i++) { - double priorX; - double thisX = xValues.get(i); - double nextX; - if (i == 0) { - nextX = xValues.get(i + 1); - priorX = thisX - (nextX - thisX); - } else if (i == xValues.size() - 1) { - priorX = xValues.get(i - 1); - nextX = thisX - priorX + thisX; - } else { - priorX = xValues.get(i - 1); - nextX = xValues.get(i + 1); - } - - double leftX = - dataXtoPixel( - xLowerAxis, - Math.max((xValues.get(i) + priorX) / 2, xLowerAxis.getRange().getBegin())); - double rectWidth = - dataXtoPixel( - xLowerAxis, - Math.min((xValues.get(i) + nextX) / 2, xLowerAxis.getRange().getEnd())) - - leftX; - - double topY = dataYtoPixel(yLeftAxis, xy.get(i).getY()); - double botY = dataYtoPixel(yLeftAxis, 0); - - // origin is at the upper left, Y increases downward - if (topY > botY) { - double tmp = topY; - topY = botY; - botY = tmp; - } - - double rectHeight = Math.abs(topY - botY); - g.setColor(ds.getColor()); - g.fillRect( - (int) (leftX + 0.5), - (int) (topY + 0.5), - (int) (rectWidth + 0.5), - (int) (rectHeight + 0.5)); - g.setColor(Color.BLACK); - g.drawRect( - (int) (leftX + 0.5), - (int) (topY + 0.5), - (int) (rectWidth + 0.5), - (int) (rectHeight + 0.5)); - } - } - - private void plotSand(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { - PointList xy = ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); - - if (xy.size() == 0) return; - - if (baseline == null) { - baseline = new PointList(); - for (double x : xy.getX()) { - baseline.add(x, Double.MIN_VALUE); - } + /** + * Plots the DataSet ds using the lower X axis and the supplied Y axis + * + * @param ds DiscreteDataSet to plot + * @param yAxis left Y axis + */ + public void plot(DiscreteDataSet ds, AxisY yAxis) { + plot(ds, xLowerAxis, yAxis); } - PointList newBaseline = new PointList(); - - GeneralPath gp = new GeneralPath(); - Point4D prior = xy.getFirst(); - double priorX = prior.getX(); - double y = prior.getY() + baseline.getY(priorX); - newBaseline.add(priorX, y); - gp.moveTo(dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, y)); - for (int i = 1; i < xy.size() - 1; i++) { - Point4D p = xy.get(i); - double x = p.getX(); - y = p.getY() + baseline.getY(x); - newBaseline.add(x, y); - gp.lineTo(dataXtoPixel(xAxis, x), dataYtoPixel(yAxis, y)); - } - Point4D next = xy.getLast(); - double nextX = next.getX(); - y = next.getY() + baseline.getY(nextX); - newBaseline.add(nextX, y); - gp.lineTo(dataXtoPixel(xAxis, nextX), dataYtoPixel(yAxis, y)); - - gp.lineTo(dataXtoPixel(xAxis, nextX), dataYtoPixel(yAxis, baseline.getY(nextX))); - for (int i = 1; i < xy.size() - 1; i++) { - Point4D p = xy.get(xy.size() - 1 - i); - gp.lineTo(dataXtoPixel(xAxis, p.getX()), dataYtoPixel(yAxis, baseline.getY(p.getX()))); + /** + * Plots the DataSet ds using the supplied axes + * + * @param ds DiscreteDataSet to plot + * @param xAxis lower X axis + * @param yAxis left Y axis + */ + public void plot(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { + switch (ds.getPlotType()) { + case LINE: + plotLine(ds, xAxis, yAxis); + break; + case BAR: + plotBar(ds, xAxis); + break; + case SAND: + plotSand(ds, xAxis, yAxis); + break; + case SYMBOL: + plotSymbol(ds, xAxis, yAxis); + break; + default: + } } - gp.lineTo(dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, baseline.getY(priorX))); - gp.lineTo( - dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, prior.getY() + baseline.getY(priorX))); - - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setClip( - new Rectangle2D.Double( - config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - g.fill(gp); - - baseline = newBaseline; - } - - private void plotSymbol(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { - PointList xy = ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); - if (xy.size() == 0) return; - - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setClip( - new Rectangle2D.Double( - config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); - if (ds.getStroke() != null) g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - - Point4D prior = xy.getFirst(); - double pixelX = dataXtoPixel(xAxis, prior.getX()); - double pixelY = dataYtoPixel(yAxis, prior.getY()); - - if (ds.getColorRamp() != null) { - if (!Double.isNaN(prior.getW())) g.setColor(ds.getColorRamp().getColor(prior.getW())); - } - ds.getSymbol().draw(g, pixelX, pixelY); - - Point2D xError = prior.getXError(); - xError = - new Point2D.Double( - dataXtoPixel(xAxis, prior.getX() - xError.getX()), - dataXtoPixel(xAxis, prior.getX() + xError.getY())); - - Point2D yError = prior.getYError(); - yError = - new Point2D.Double( - dataYtoPixel(yAxis, prior.getY() - yError.getX()), - dataYtoPixel(yAxis, prior.getY() + yError.getY())); - - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); - - for (int i = 1; i < xy.size() - 1; i++) { - Point4D p = xy.get(i); - pixelX = dataXtoPixel(xAxis, p.getX()); - pixelY = dataYtoPixel(yAxis, p.getY()); - - if (ds.getColorRamp() != null) { - if (!Double.isNaN(p.getW())) g.setColor(ds.getColorRamp().getColor(p.getW())); - } - - ds.getSymbol().draw(g, pixelX, pixelY); - xError = p.getXError(); - xError = - new Point2D.Double( - dataXtoPixel(xAxis, p.getX() - xError.getX()), - dataXtoPixel(xAxis, p.getX() + xError.getY())); - - yError = p.getYError(); - yError = - new Point2D.Double( - dataYtoPixel(yAxis, p.getY() - yError.getX()), - dataYtoPixel(yAxis, p.getY() + yError.getY())); - - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + /** + * Draw a filled shape on the plot + * + * @param color fill color + * @param outline outline to fill + */ + public void plot(Color color, List outline) { + plot(color, outline, true); } - Point4D next = xy.getLast(); - pixelX = dataXtoPixel(xAxis, next.getX()); - pixelY = dataYtoPixel(yAxis, next.getY()); + /** + * Draw a shape on the plot + * + * @param color shape color + * @param outline outline to draw + * @param fill if true, fill outline + */ + public void plot(Color color, List outline, boolean fill) { + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(color); + g.setClip(new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); - if (ds.getColorRamp() != null) { - if (!Double.isNaN(next.getW())) g.setColor(ds.getColorRamp().getColor(next.getW())); + GeneralPath gp = new GeneralPath(); + Point2D xy; + for (Point2D point2D : outline) { + xy = point2D; + if (xy == null) continue; + + double pixelX = xLowerAxis.dataToPixel(config.getLeftPlotEdge(), config.getRightPlotEdge(), xy.getX()); + double pixelY = yLeftAxis.dataToPixel(config.getBottomPlotEdge(), config.getTopPlotEdge(), xy.getY()); + + if (gp.getCurrentPoint() == null) gp.moveTo(pixelX, pixelY); + else gp.lineTo(pixelX, pixelY); + } + + if (gp.getCurrentPoint() != null) { + gp.closePath(); + if (fill) g.fill(gp); + g.draw(gp); + } } - ds.getSymbol().draw(g, pixelX, pixelY); - xError = next.getXError(); - if (xError != null) { - xError = - new Point2D.Double( - dataXtoPixel(xAxis, next.getX() - xError.getX()), - dataXtoPixel(xAxis, next.getX() + xError.getY())); + private double roundToNearestTenth(double x) { + return Math.round(x * 10) / 10.; } - yError = next.getYError(); - if (yError != null) { - yError = - new Point2D.Double( - dataYtoPixel(yAxis, next.getY() - yError.getX()), - dataYtoPixel(yAxis, next.getY() + yError.getY())); + + private void plotLine(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { + PointList xy = ds.getData(); + if (xy.size() == 0) return; + + Point4D prior = xy.getFirst(); + + // Store points to plot if they differ from the previous point by more than a tenth of a pixel. + // This can save a lot of memory if there are a lot of points. + List pointMap = new ArrayList<>(); + Point2D lastPoint = new Point2D.Double( + roundToNearestTenth(dataXtoPixel(xAxis, prior.getX())), + roundToNearestTenth(dataYtoPixel(yAxis, prior.getY()))); + pointMap.add(lastPoint); + for (int i = 1; i < xy.size(); i++) { + Point4D p = xy.get(i); + Point2D nextPoint = new Point2D.Double( + roundToNearestTenth(dataXtoPixel(xAxis, p.getX())), + roundToNearestTenth(dataYtoPixel(yAxis, p.getY()))); + if (lastPoint.getX() != nextPoint.getX() || lastPoint.getY() != nextPoint.getY()) { + pointMap.add(nextPoint); + lastPoint = nextPoint; + } + } + + GeneralPath gp = new GeneralPath(); + gp.moveTo(pointMap.get(0).getX(), pointMap.get(0).getY()); + for (int i = 1; i < pointMap.size(); i++) { + gp.lineTo(pointMap.get(i).getX(), pointMap.get(i).getY()); + } + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setClip(new Rectangle2D.Double( + config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); + g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + g.draw(gp); + } + + private void plotBar(DiscreteDataSet ds, AxisX xAxis) { + PointList xy = + ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); + if (xy.size() == 0) return; + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setClip(new Rectangle2D.Double( + config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); + + List xValues = xy.getX(); + // need a prior and next for each point + if (xValues.size() < 3) return; + + for (int i = 0; i < xValues.size(); i++) { + double priorX; + double thisX = xValues.get(i); + double nextX; + if (i == 0) { + nextX = xValues.get(i + 1); + priorX = thisX - (nextX - thisX); + } else if (i == xValues.size() - 1) { + priorX = xValues.get(i - 1); + nextX = thisX - priorX + thisX; + } else { + priorX = xValues.get(i - 1); + nextX = xValues.get(i + 1); + } + + double leftX = dataXtoPixel( + xLowerAxis, + Math.max( + (xValues.get(i) + priorX) / 2, xLowerAxis.getRange().getBegin())); + double rectWidth = dataXtoPixel( + xLowerAxis, + Math.min( + (xValues.get(i) + nextX) / 2, + xLowerAxis.getRange().getEnd())) + - leftX; + + double topY = dataYtoPixel(yLeftAxis, xy.get(i).getY()); + double botY = dataYtoPixel(yLeftAxis, 0); + + // origin is at the upper left, Y increases downward + if (topY > botY) { + double tmp = topY; + topY = botY; + botY = tmp; + } + + double rectHeight = Math.abs(topY - botY); + g.setColor(ds.getColor()); + g.fillRect((int) (leftX + 0.5), (int) (topY + 0.5), (int) (rectWidth + 0.5), (int) (rectHeight + 0.5)); + g.setColor(Color.BLACK); + g.drawRect((int) (leftX + 0.5), (int) (topY + 0.5), (int) (rectWidth + 0.5), (int) (rectHeight + 0.5)); + } + } + + private void plotSand(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { + PointList xy = + ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); + + if (xy.size() == 0) return; + + if (baseline == null) { + baseline = new PointList(); + for (double x : xy.getX()) { + baseline.add(x, Double.MIN_VALUE); + } + } + + PointList newBaseline = new PointList(); + + GeneralPath gp = new GeneralPath(); + Point4D prior = xy.getFirst(); + double priorX = prior.getX(); + double y = prior.getY() + baseline.getY(priorX); + newBaseline.add(priorX, y); + gp.moveTo(dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, y)); + for (int i = 1; i < xy.size() - 1; i++) { + Point4D p = xy.get(i); + double x = p.getX(); + y = p.getY() + baseline.getY(x); + newBaseline.add(x, y); + gp.lineTo(dataXtoPixel(xAxis, x), dataYtoPixel(yAxis, y)); + } + Point4D next = xy.getLast(); + double nextX = next.getX(); + y = next.getY() + baseline.getY(nextX); + newBaseline.add(nextX, y); + gp.lineTo(dataXtoPixel(xAxis, nextX), dataYtoPixel(yAxis, y)); + + gp.lineTo(dataXtoPixel(xAxis, nextX), dataYtoPixel(yAxis, baseline.getY(nextX))); + for (int i = 1; i < xy.size() - 1; i++) { + Point4D p = xy.get(xy.size() - 1 - i); + gp.lineTo(dataXtoPixel(xAxis, p.getX()), dataYtoPixel(yAxis, baseline.getY(p.getX()))); + } + + gp.lineTo(dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, baseline.getY(priorX))); + gp.lineTo(dataXtoPixel(xAxis, priorX), dataYtoPixel(yAxis, prior.getY() + baseline.getY(priorX))); + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setClip(new Rectangle2D.Double( + config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); + g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + g.fill(gp); + + baseline = newBaseline; + } + + private void plotSymbol(DiscreteDataSet ds, AxisX xAxis, AxisY yAxis) { + PointList xy = + ds.getData().subSetX(xAxis.getRange().getMin(), xAxis.getRange().getMax()); + if (xy.size() == 0) return; + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setClip(new Rectangle2D.Double( + config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height())); + if (ds.getStroke() != null) g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + + Point4D prior = xy.getFirst(); + double pixelX = dataXtoPixel(xAxis, prior.getX()); + double pixelY = dataYtoPixel(yAxis, prior.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(prior.getW())) g.setColor(ds.getColorRamp().getColor(prior.getW())); + } + ds.getSymbol().draw(g, pixelX, pixelY); + + Point2D xError = prior.getXError(); + xError = new Point2D.Double( + dataXtoPixel(xAxis, prior.getX() - xError.getX()), dataXtoPixel(xAxis, prior.getX() + xError.getY())); + + Point2D yError = prior.getYError(); + yError = new Point2D.Double( + dataYtoPixel(yAxis, prior.getY() - yError.getX()), dataYtoPixel(yAxis, prior.getY() + yError.getY())); + + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + + for (int i = 1; i < xy.size() - 1; i++) { + Point4D p = xy.get(i); + pixelX = dataXtoPixel(xAxis, p.getX()); + pixelY = dataYtoPixel(yAxis, p.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(p.getW())) g.setColor(ds.getColorRamp().getColor(p.getW())); + } + + ds.getSymbol().draw(g, pixelX, pixelY); + xError = p.getXError(); + xError = new Point2D.Double( + dataXtoPixel(xAxis, p.getX() - xError.getX()), dataXtoPixel(xAxis, p.getX() + xError.getY())); + + yError = p.getYError(); + yError = new Point2D.Double( + dataYtoPixel(yAxis, p.getY() - yError.getX()), dataYtoPixel(yAxis, p.getY() + yError.getY())); + + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + } + + Point4D next = xy.getLast(); + pixelX = dataXtoPixel(xAxis, next.getX()); + pixelY = dataYtoPixel(yAxis, next.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(next.getW())) g.setColor(ds.getColorRamp().getColor(next.getW())); + } + + ds.getSymbol().draw(g, pixelX, pixelY); + xError = next.getXError(); + if (xError != null) { + xError = new Point2D.Double( + dataXtoPixel(xAxis, next.getX() - xError.getX()), dataXtoPixel(xAxis, next.getX() + xError.getY())); + } + yError = next.getYError(); + if (yError != null) { + yError = new Point2D.Double( + dataYtoPixel(yAxis, next.getY() - yError.getX()), dataYtoPixel(yAxis, next.getY() + yError.getY())); + } + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); } - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/MapPlot.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/MapPlot.java index 340b40f..a2bc2e4 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/MapPlot.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/MapPlot.java @@ -61,478 +61,466 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class MapPlot extends AreaPlot { - private final Projection proj; - private ProjectionRectangular sourceMapProj; - private final List offsets; - public MapPlot(PlotConfig config, Projection p) { - super(config); - proj = p; - sourceMapProj = null; + private final Projection proj; + private ProjectionRectangular sourceMapProj; + private final List offsets; - offsets = new ArrayList<>(); - offsets.add(0); - if (proj.isWrapAround()) { - offsets.add(-width); - offsets.add(width); - } - } + public MapPlot(PlotConfig config, Projection p) { + super(config); + proj = p; + sourceMapProj = null; - /** - * Set background using a rectangular map - * - * @param sourceMap image file - * @param lv center point of the map - */ - public void setBackgroundMap(BufferedImage sourceMap, LatitudinalVector lv) { - - sourceMapProj = new ProjectionRectangular(sourceMap.getWidth(), sourceMap.getHeight(), lv); - - for (int i = 0; i < proj.getWidth(); i++) { - for (int j = 0; j < proj.getHeight(); j++) { - LatitudinalVector ll = proj.pixelToSpherical(i, j); - if (ll != null) { - Point2D.Double xy = sourceMapProj.sphericalToPixel(ll); - int argb = sourceMap.getRGB((int) xy.getX(), (int) xy.getY()); - image.setRGB(config.leftMargin() + i, config.topMargin() + j, argb); + offsets = new ArrayList<>(); + offsets.add(0); + if (proj.isWrapAround()) { + offsets.add(-width); + offsets.add(width); } - } - } - } - - /** - * @param latSpacing spacing between latitude lines, in radians - * @param lonSpacing spacing between longitude lines, in radians - * @param annotate mark lat/lon lines - */ - public void drawLatLonGrid(double latSpacing, double lonSpacing, boolean annotate) { - - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(config.gridColor()); - g.setClip( - new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); - - NavigableSet lats = new TreeSet<>(); - NavigableSet lons = new TreeSet<>(); - - for (double lat = -Math.PI / 2; lat < Math.PI / 2; lat += latSpacing) lats.add(lat); - - for (double lon = 0; lon < 2 * Math.PI; lon += lonSpacing) lons.add(lon); - - NavigableMap latLengthMap = new TreeMap<>(); - for (double lat : lats) { - double length = drawLatitudeLine(lat); - latLengthMap.put(length, lat); } - NavigableMap lonLengthMap = new TreeMap<>(); - for (double lon : lons) { - double length = drawLongitudeLine(lon); - lonLengthMap.put(length, lon); - } + /** + * Set background using a rectangular map + * + * @param sourceMap image file + * @param lv center point of the map + */ + public void setBackgroundMap(BufferedImage sourceMap, LatitudinalVector lv) { - if (annotate) { - g.setFont(config.gridFont()); + sourceMapProj = new ProjectionRectangular(sourceMap.getWidth(), sourceMap.getHeight(), lv); - for (double lat : lats) { - - // along each latitude line, draw the label on the longest longitude line - double lon = lonLengthMap.lastEntry().getValue(); - - NavigableSet lonCrossings = - new TreeSet<>(Comparator.comparingDouble(Point2D::getX)); - Point2D xy = proj.sphericalToPixel(lat, lon + lonSpacing / 2); - if (xy != null) lonCrossings.add(xy); - xy = proj.sphericalToPixel(lat, lon - lonSpacing / 2); - if (xy != null) lonCrossings.add(xy); - - if (!lonCrossings.isEmpty()) { - Point2D p = lonCrossings.first(); - addAnnotation( - g, - StringFunctions.toDegreesLat("%.0f").apply(lat), - p.getX() + config.leftMargin(), - p.getY() + config.topMargin(), - Keyword.ALIGN_BOTTOM, - Keyword.ALIGN_CENTER); + for (int i = 0; i < proj.getWidth(); i++) { + for (int j = 0; j < proj.getHeight(); j++) { + LatitudinalVector ll = proj.pixelToSpherical(i, j); + if (ll != null) { + Point2D.Double xy = sourceMapProj.sphericalToPixel(ll); + int argb = sourceMap.getRGB((int) xy.getX(), (int) xy.getY()); + image.setRGB(config.leftMargin() + i, config.topMargin() + j, argb); + } + } } - } - - for (double lon : lons) { - - // along each longitude line, draw the label on the longest latitude line - double lat = latLengthMap.lastEntry().getValue(); - - NavigableSet latCrossings = - new TreeSet<>(Comparator.comparingDouble(Point2D::getY)); - - // draw the label closer to the equator - double labelLat = (lat < 0) ? lat + latSpacing / 2 : lat - latSpacing / 2; - Point2D xy = proj.sphericalToPixel(labelLat, lon); - if (xy != null) latCrossings.add(xy); - - if (!latCrossings.isEmpty()) { - Point2D p = latCrossings.last(); - addAnnotation( - g, - StringFunctions.toDegreesELon("%.0f").apply(lon), - p.getX() + config.leftMargin(), - p.getY() + config.topMargin(), - Keyword.ALIGN_BOTTOM, - Keyword.ALIGN_CENTER); - } - } } - } - /** - * @param lat in radians - * @return length of the longest segment drawn, in pixels (line may be broken at 0 longitude) - */ - public double drawLatitudeLine(double lat) { - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(config.gridColor()); - g.setClip( - new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); + /** + * @param latSpacing spacing between latitude lines, in radians + * @param lonSpacing spacing between longitude lines, in radians + * @param annotate mark lat/lon lines + */ + public void drawLatLonGrid(double latSpacing, double lonSpacing, boolean annotate) { - double longestLength = 0; - double length = 0; - Point2D lastPoint = null; - GeneralPath gp = new GeneralPath(); - final double spacing = proj.radiansPerPixel(); - for (double lon = 0; lon < 2 * Math.PI; lon += spacing) { - Point2D xy = proj.sphericalToPixel(lat, lon); - if (xy == null) { - if (gp.getCurrentPoint() != null) { - g.draw(gp); - gp = new GeneralPath(); - if (length > longestLength) { + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(config.gridColor()); + g.setClip(new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); + + NavigableSet lats = new TreeSet<>(); + NavigableSet lons = new TreeSet<>(); + + for (double lat = -Math.PI / 2; lat < Math.PI / 2; lat += latSpacing) lats.add(lat); + + for (double lon = 0; lon < 2 * Math.PI; lon += lonSpacing) lons.add(lon); + + NavigableMap latLengthMap = new TreeMap<>(); + for (double lat : lats) { + double length = drawLatitudeLine(lat); + latLengthMap.put(length, lat); + } + + NavigableMap lonLengthMap = new TreeMap<>(); + for (double lon : lons) { + double length = drawLongitudeLine(lon); + lonLengthMap.put(length, lon); + } + + if (annotate) { + g.setFont(config.gridFont()); + + for (double lat : lats) { + + // along each latitude line, draw the label on the longest longitude line + double lon = lonLengthMap.lastEntry().getValue(); + + NavigableSet lonCrossings = new TreeSet<>(Comparator.comparingDouble(Point2D::getX)); + Point2D xy = proj.sphericalToPixel(lat, lon + lonSpacing / 2); + if (xy != null) lonCrossings.add(xy); + xy = proj.sphericalToPixel(lat, lon - lonSpacing / 2); + if (xy != null) lonCrossings.add(xy); + + if (!lonCrossings.isEmpty()) { + Point2D p = lonCrossings.first(); + addAnnotation( + g, + StringFunctions.toDegreesLat("%.0f").apply(lat), + p.getX() + config.leftMargin(), + p.getY() + config.topMargin(), + Keyword.ALIGN_BOTTOM, + Keyword.ALIGN_CENTER); + } + } + + for (double lon : lons) { + + // along each longitude line, draw the label on the longest latitude line + double lat = latLengthMap.lastEntry().getValue(); + + NavigableSet latCrossings = new TreeSet<>(Comparator.comparingDouble(Point2D::getY)); + + // draw the label closer to the equator + double labelLat = (lat < 0) ? lat + latSpacing / 2 : lat - latSpacing / 2; + Point2D xy = proj.sphericalToPixel(labelLat, lon); + if (xy != null) latCrossings.add(xy); + + if (!latCrossings.isEmpty()) { + Point2D p = latCrossings.last(); + addAnnotation( + g, + StringFunctions.toDegreesELon("%.0f").apply(lon), + p.getX() + config.leftMargin(), + p.getY() + config.topMargin(), + Keyword.ALIGN_BOTTOM, + Keyword.ALIGN_CENTER); + } + } + } + } + + /** + * @param lat in radians + * @return length of the longest segment drawn, in pixels (line may be broken at 0 longitude) + */ + public double drawLatitudeLine(double lat) { + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(config.gridColor()); + g.setClip(new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); + + double longestLength = 0; + double length = 0; + Point2D lastPoint = null; + GeneralPath gp = new GeneralPath(); + final double spacing = proj.radiansPerPixel(); + for (double lon = 0; lon < 2 * Math.PI; lon += spacing) { + Point2D xy = proj.sphericalToPixel(lat, lon); + if (xy == null) { + if (gp.getCurrentPoint() != null) { + g.draw(gp); + gp = new GeneralPath(); + if (length > longestLength) { + longestLength = length; + } + } + } else { + if (gp.getCurrentPoint() == null) { + gp.moveTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); + } else { + if (lastPoint != null) { + gp.lineTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); + double dx = xy.getX() - lastPoint.getX(); + double dy = xy.getY() - lastPoint.getY(); + length += Math.sqrt(dx * dx + dy * dy); + } + } + lastPoint = xy; + } + } + if (gp.getCurrentPoint() != null) g.draw(gp); + if (length > longestLength) { longestLength = length; - } } - } else { - if (gp.getCurrentPoint() == null) { - gp.moveTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - } else { - if (lastPoint != null) { - gp.lineTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - double dx = xy.getX() - lastPoint.getX(); - double dy = xy.getY() - lastPoint.getY(); - length += Math.sqrt(dx * dx + dy * dy); - } - } - lastPoint = xy; - } - } - if (gp.getCurrentPoint() != null) g.draw(gp); - if (length > longestLength) { - longestLength = length; - } - return longestLength; - } - - /** - * @param lon in radians - * @return length of line drawn - */ - public double drawLongitudeLine(double lon) { - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(config.gridColor()); - g.setClip( - new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); - - double length = 0; - Point2D lastPoint = null; - GeneralPath gp = new GeneralPath(); - final double spacing = proj.radiansPerPixel(); - for (double lat = -Math.PI / 2; lat < Math.PI / 2; lat += spacing) { - Point2D xy = proj.sphericalToPixel(lat, lon); - if (xy != null) { - if (gp.getCurrentPoint() == null) - gp.moveTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - else { - if (lastPoint != null) { - gp.lineTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - double dx = xy.getX() - lastPoint.getX(); - double dy = xy.getY() - lastPoint.getY(); - length += Math.sqrt(dx * dx + dy * dy); - } - } - lastPoint = xy; - } - } - if (gp.getCurrentPoint() != null) g.draw(gp); - - return length; - } - - /** - * @param center center - * @param radius radius, measured as an angle from the center of the body - * @param step angular step along the circumference. e.g. if step is pi/180, there will be one - * point per degree. - * @param color color - */ - public void drawCircle(LatitudinalVector center, double radius, double step, Color color) { - List list = getCircle(center, radius, step); - DiscreteDataSet outline = new DiscreteDataSet(""); - outline.setColor(color); - for (Point2D p : list) outline.add(p.getX(), p.getY()); - plot(outline); - } - - /** - * @param center center - * @param radius radius, measured as an angle from the center of the body - * @param step angular step along the circumference. e.g. if step is pi/180, there will be one - * point per degree. - * @param color color - */ - public void drawFilledCircle(LatitudinalVector center, double radius, double step, Color color) { - List list = getCircle(center, radius, step); - plot(color, list, true); - } - - /** - * @param center center - * @param radius radius, measured as an angle from the center of the body - * @param step angular step along the circumference. e.g. if step is pi/180, there will be one - * point per degree. - * @return return a list of points along the circumference. - */ - private List getCircle(LatitudinalVector center, double radius, double step) { - UnwritableVectorIJK centerIJK = CoordConverters.convert(center); - - // find a vector perpendicular to center - UnwritableVectorIJK axis = VectorIJK.cross(centerIJK, VectorIJK.K); - if (Math.abs(Math.sin(centerIJK.getSeparation(VectorIJK.K))) < 0.1) - axis = VectorIJK.cross(centerIJK, VectorIJK.J); - - // find any point separated by radius from the center - AxisAndAngle aaa = new AxisAndAngle(axis, radius); - UnwritableVectorIJK border = aaa.getRotation(new RotationMatrixIJK()).mxv(centerIJK); - LatitudinalVector lv = CoordConverters.convertToLatitudinal(border); - aaa = new AxisAndAngle(centerIJK, step); - - List list = new ArrayList<>(); - list.add(new Point2D.Double(lv.getLongitude(), lv.getLatitude())); - for (double angle = 0; angle <= 2 * Math.PI; angle += step) { - border = aaa.getRotation(new RotationMatrixIJK()).mxv(border); - lv = CoordConverters.convertToLatitudinal(border); - list.add( - new Point2D.Double( - MathUtils.normalizeAngle(lv.getLongitude(), center.getLongitude()), - lv.getLatitude())); + return longestLength; } - return list; - } + /** + * @param lon in radians + * @return length of line drawn + */ + public double drawLongitudeLine(double lon) { + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(config.gridColor()); + g.setClip(new Rectangle(config.leftMargin(), config.topMargin(), config.width(), config.height())); - /** - * Map is assumed to be in rectangular projection and have its upper left corner at lon -180, lat - * 90 and lower right corner at lon 180, lat -90 - * - * @param sourceMap image - */ - public void setBackgroundMap(BufferedImage sourceMap) { - setBackgroundMap(sourceMap, new LatitudinalVector(1, 0, 0)); - } - - /** - * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied ranges - * will not be plotted. - * - * @param func input coordinates to func are longitude and latitude in radians - * @param ramp color ramp - * @param xRange allowed longitude range in radians - * @param yRange allowed latitude range in radians - */ - public void plot(MultivariateFunction func, ColorRamp ramp, AxisRange xRange, AxisRange yRange) { - for (int i = 0; i < config.width(); i++) { - for (int j = 0; j < config.height(); j++) { - LatitudinalVector ll = proj.pixelToSpherical(i, j); - if (ll == null) continue; - if (xRange.closedContains(ll.getLongitude())) { - if (yRange.closedContains(ll.getLatitude())) { - double value = func.value(new double[] {ll.getLongitude(), ll.getLatitude()}); - Color color = ramp.getColor(value); - if (color.getAlpha() > 0) - image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); - } + double length = 0; + Point2D lastPoint = null; + GeneralPath gp = new GeneralPath(); + final double spacing = proj.radiansPerPixel(); + for (double lat = -Math.PI / 2; lat < Math.PI / 2; lat += spacing) { + Point2D xy = proj.sphericalToPixel(lat, lon); + if (xy != null) { + if (gp.getCurrentPoint() == null) + gp.moveTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); + else { + if (lastPoint != null) { + gp.lineTo(xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); + double dx = xy.getX() - lastPoint.getX(); + double dy = xy.getY() - lastPoint.getY(); + length += Math.sqrt(dx * dx + dy * dy); + } + } + lastPoint = xy; + } } - } + if (gp.getCurrentPoint() != null) g.draw(gp); + + return length; } - } - @Override - public void plot( - MultivariateFunction func, - ColorRamp ramp, - AxisX xAxis, - AxisY yAxis, - AxisRange xRange, - AxisRange yRange) { - plot(func, ramp, xRange, yRange); - } - - @Override - public void plot(Color color, List outline) { - plot(color, outline, true); - } - - @Override - public void plot(Color color, List outline, boolean fill) { - - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(color); - g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); - - for (double offset : offsets) { - GeneralPath gp = new GeneralPath(); - double lastX = 0; - for (Point2D point2D : outline) { - Point2D xy = proj.sphericalToPixel(point2D.getY(), point2D.getX()); - if (xy == null) continue; - - double nextX = xy.getX() + config.leftMargin() + offset; - double nextY = xy.getY() + config.topMargin(); - if (gp.getCurrentPoint() == null) { - gp.moveTo(nextX, nextY); - } else { - if (proj.isWrapAround()) { - if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX + width))) nextX += width; - if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX - width))) nextX -= width; - } - gp.lineTo(nextX, nextY); - } - lastX = nextX; - } - - if (gp.getCurrentPoint() != null) { - gp.closePath(); - Rectangle2D bounds = gp.getBounds2D(); - if (bounds.getWidth() < config.width() / 2.) { - if (fill) g.fill(gp); - g.draw(gp); - } - } + /** + * @param center center + * @param radius radius, measured as an angle from the center of the body + * @param step angular step along the circumference. e.g. if step is pi/180, there will be one + * point per degree. + * @param color color + */ + public void drawCircle(LatitudinalVector center, double radius, double step, Color color) { + List list = getCircle(center, radius, step); + DiscreteDataSet outline = new DiscreteDataSet(""); + outline.setColor(color); + for (Point2D p : list) outline.add(p.getX(), p.getY()); + plot(outline); } - } - @Override - public void addAnnotations(Annotations annotations) { + /** + * @param center center + * @param radius radius, measured as an angle from the center of the body + * @param step angular step along the circumference. e.g. if step is pi/180, there will be one + * point per degree. + * @param color color + */ + public void drawFilledCircle(LatitudinalVector center, double radius, double step, Color color) { + List list = getCircle(center, radius, step); + plot(color, list, true); + } - for (double offset : offsets) { - for (Point4D p : annotations) { - Annotation a = annotations.getAnnotation(p); + /** + * @param center center + * @param radius radius, measured as an angle from the center of the body + * @param step angular step along the circumference. e.g. if step is pi/180, there will be one + * point per degree. + * @return return a list of points along the circumference. + */ + private List getCircle(LatitudinalVector center, double radius, double step) { + UnwritableVectorIJK centerIJK = CoordConverters.convert(center); - Point2D xy = proj.sphericalToPixel(p.getY(), p.getX()); + // find a vector perpendicular to center + UnwritableVectorIJK axis = VectorIJK.cross(centerIJK, VectorIJK.K); + if (Math.abs(Math.sin(centerIJK.getSeparation(VectorIJK.K))) < 0.1) + axis = VectorIJK.cross(centerIJK, VectorIJK.J); + + // find any point separated by radius from the center + AxisAndAngle aaa = new AxisAndAngle(axis, radius); + UnwritableVectorIJK border = aaa.getRotation(new RotationMatrixIJK()).mxv(centerIJK); + LatitudinalVector lv = CoordConverters.convertToLatitudinal(border); + aaa = new AxisAndAngle(centerIJK, step); + + List list = new ArrayList<>(); + list.add(new Point2D.Double(lv.getLongitude(), lv.getLatitude())); + for (double angle = 0; angle <= 2 * Math.PI; angle += step) { + border = aaa.getRotation(new RotationMatrixIJK()).mxv(border); + lv = CoordConverters.convertToLatitudinal(border); + list.add(new Point2D.Double( + MathUtils.normalizeAngle(lv.getLongitude(), center.getLongitude()), lv.getLatitude())); + } + + return list; + } + + /** + * Map is assumed to be in rectangular projection and have its upper left corner at lon -180, lat + * 90 and lower right corner at lon 180, lat -90 + * + * @param sourceMap image + */ + public void setBackgroundMap(BufferedImage sourceMap) { + setBackgroundMap(sourceMap, new LatitudinalVector(1, 0, 0)); + } + + /** + * Plot the 2D function using the ramp and supplied axes. Coordinates outside the supplied ranges + * will not be plotted. + * + * @param func input coordinates to func are longitude and latitude in radians + * @param ramp color ramp + * @param xRange allowed longitude range in radians + * @param yRange allowed latitude range in radians + */ + public void plot(MultivariateFunction func, ColorRamp ramp, AxisRange xRange, AxisRange yRange) { + for (int i = 0; i < config.width(); i++) { + for (int j = 0; j < config.height(); j++) { + LatitudinalVector ll = proj.pixelToSpherical(i, j); + if (ll == null) continue; + if (xRange.closedContains(ll.getLongitude())) { + if (yRange.closedContains(ll.getLatitude())) { + double value = func.value(new double[] {ll.getLongitude(), ll.getLatitude()}); + Color color = ramp.getColor(value); + if (color.getAlpha() > 0) + image.setRGB(config.leftMargin() + i, config.topMargin() + j, color.getRGB()); + } + } + } + } + } + + @Override + public void plot( + MultivariateFunction func, ColorRamp ramp, AxisX xAxis, AxisY yAxis, AxisRange xRange, AxisRange yRange) { + plot(func, ramp, xRange, yRange); + } + + @Override + public void plot(Color color, List outline) { + plot(color, outline, true); + } + + @Override + public void plot(Color color, List outline, boolean fill) { + + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(color); + g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); + + for (double offset : offsets) { + GeneralPath gp = new GeneralPath(); + double lastX = 0; + for (Point2D point2D : outline) { + Point2D xy = proj.sphericalToPixel(point2D.getY(), point2D.getX()); + if (xy == null) continue; + + double nextX = xy.getX() + config.leftMargin() + offset; + double nextY = xy.getY() + config.topMargin(); + if (gp.getCurrentPoint() == null) { + gp.moveTo(nextX, nextY); + } else { + if (proj.isWrapAround()) { + if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX + width))) nextX += width; + if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX - width))) nextX -= width; + } + gp.lineTo(nextX, nextY); + } + lastX = nextX; + } + + if (gp.getCurrentPoint() != null) { + gp.closePath(); + Rectangle2D bounds = gp.getBounds2D(); + if (bounds.getWidth() < config.width() / 2.) { + if (fill) g.fill(gp); + g.draw(gp); + } + } + } + } + + @Override + public void addAnnotations(Annotations annotations) { + + for (double offset : offsets) { + for (Point4D p : annotations) { + Annotation a = annotations.getAnnotation(p); + + Point2D xy = proj.sphericalToPixel(p.getY(), p.getX()); + if (xy != null) { + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + + g.setColor(a.color()); + g.setFont(a.font()); + g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); + + addAnnotation( + g, + a.text(), + xy.getX() + config.leftMargin() + offset, + xy.getY() + config.topMargin(), + a.verticalAlignment(), + a.horizontalAlignment()); + } + } + } + } + + /** + * @param ds dataset to plot + */ + @Override + public void plot(DiscreteDataSet ds) { + switch (ds.getPlotType()) { + case LINE: + plotLine(ds); + break; + case SYMBOL: + plotSymbol(ds); + break; + default: + } + } + + private void plotLine(DiscreteDataSet ds) { + + PointList points = ds.getData(); + if (points.size() == 0) return; + + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); + + for (double offset : offsets) { + GeneralPath gp = new GeneralPath(); + double lastX = 0; + for (int i = 0; i < points.size(); i++) { + Point2D xy = proj.sphericalToPixel( + points.get(i).getY(), points.get(i).getX()); + if (xy == null) continue; + double nextX = xy.getX() + config.leftMargin() + offset; + double nextY = xy.getY() + config.topMargin(); + if (gp.getCurrentPoint() == null) { + gp.moveTo(nextX, nextY); + } else { + if (proj.isWrapAround()) { + if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX + width))) nextX += width; + if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX - width))) nextX -= width; + } + gp.lineTo(nextX, nextY); + } + lastX = nextX; + } + g.draw(gp); + } + } + + private void plotSymbol(DiscreteDataSet ds) { + PointList points = ds.getData(); + if (points.size() == 0) return; + + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); + + Point4D prior = points.getFirst(); + Point2D xy = proj.sphericalToPixel(prior.getY(), prior.getX()); if (xy != null) { - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - - g.setColor(a.color()); - g.setFont(a.font()); - g.setClip( - config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); - - addAnnotation( - g, - a.text(), - xy.getX() + config.leftMargin() + offset, - xy.getY() + config.topMargin(), - a.verticalAlignment(), - a.horizontalAlignment()); + if (ds.getColorRamp() != null) { + if (!Double.isNaN(prior.getW())) g.setColor(ds.getColorRamp().getColor(prior.getW())); + } + ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); } - } - } - } - - /** - * @param ds dataset to plot - */ - @Override - public void plot(DiscreteDataSet ds) { - switch (ds.getPlotType()) { - case LINE: - plotLine(ds); - break; - case SYMBOL: - plotSymbol(ds); - break; - default: - } - } - - private void plotLine(DiscreteDataSet ds) { - - PointList points = ds.getData(); - if (points.size() == 0) return; - - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); - - for (double offset : offsets) { - GeneralPath gp = new GeneralPath(); - double lastX = 0; - for (int i = 0; i < points.size(); i++) { - Point2D xy = proj.sphericalToPixel(points.get(i).getY(), points.get(i).getX()); - if (xy == null) continue; - double nextX = xy.getX() + config.leftMargin() + offset; - double nextY = xy.getY() + config.topMargin(); - if (gp.getCurrentPoint() == null) { - gp.moveTo(nextX, nextY); - } else { - if (proj.isWrapAround()) { - if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX + width))) nextX += width; - if (Math.abs(lastX - nextX) > Math.abs(lastX - (nextX - width))) nextX -= width; - } - gp.lineTo(nextX, nextY); + for (int i = 1; i < points.size() - 1; i++) { + Point4D p = points.get(i); + xy = proj.sphericalToPixel(p.getY(), p.getX()); + if (xy != null) { + if (ds.getColorRamp() != null) { + if (!Double.isNaN(p.getW())) g.setColor(ds.getColorRamp().getColor(p.getW())); + } + ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); + } } - lastX = nextX; - } - g.draw(gp); + + Point4D next = points.getLast(); + xy = proj.sphericalToPixel(next.getY(), next.getX()); + if (xy != null) ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); } - } - - private void plotSymbol(DiscreteDataSet ds) { - PointList points = ds.getData(); - if (points.size() == 0) return; - - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - g.setClip(config.getLeftPlotEdge(), config.getTopPlotEdge(), config.width(), config.height()); - - Point4D prior = points.getFirst(); - Point2D xy = proj.sphericalToPixel(prior.getY(), prior.getX()); - if (xy != null) { - if (ds.getColorRamp() != null) { - if (!Double.isNaN(prior.getW())) g.setColor(ds.getColorRamp().getColor(prior.getW())); - } - ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - } - for (int i = 1; i < points.size() - 1; i++) { - Point4D p = points.get(i); - xy = proj.sphericalToPixel(p.getY(), p.getX()); - if (xy != null) { - if (ds.getColorRamp() != null) { - if (!Double.isNaN(p.getW())) g.setColor(ds.getColorRamp().getColor(p.getW())); - } - ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - } - } - - Point4D next = points.getLast(); - xy = proj.sphericalToPixel(next.getY(), next.getX()); - if (xy != null) - ds.getSymbol().draw(g, xy.getX() + config.leftMargin(), xy.getY() + config.topMargin()); - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/PlotCanvas.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/PlotCanvas.java index b268558..7eb4160 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/PlotCanvas.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/PlotCanvas.java @@ -60,539 +60,517 @@ import terrasaur.utils.saaPlotLib.util.StringUtils; * @author nairah1 */ public abstract class PlotCanvas { - protected final BufferedImage image; - protected final int width, height; - protected final int pageWidth, pageHeight; - protected final PlotConfig config; - protected List legend; + protected final BufferedImage image; + protected final int width, height; + protected final int pageWidth, pageHeight; + protected final PlotConfig config; + protected List legend; - /** - * @return width of the plot area. Does not include margins. - */ - public int getWidth() { - return width; - } - - /** - * @return height of the plot area. Does not include margins. - */ - public int getHeight() { - return height; - } - - /** - * @return width of the entire page, including left and right margins. - */ - public int getPageWidth() { - return pageWidth; - } - - /** - * @return height of the entire page, including top and bottom margins. - */ - public int getPageHeight() { - return pageHeight; - } - - public PlotCanvas(PlotConfig config) { - super(); - this.config = config; - this.legend = new ArrayList<>(); - - this.width = config.width(); - this.height = config.height(); - this.pageWidth = config.width() + config.leftMargin() + config.rightMargin(); - this.pageHeight = config.height() + config.topMargin() + config.bottomMargin(); - this.image = new BufferedImage(pageWidth, pageHeight, BufferedImage.TYPE_INT_ARGB); - - Graphics2D g = image.createGraphics(); - g.setComposite(AlphaComposite.Clear); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - g.setComposite(AlphaComposite.Src); - g.setColor(config.backgroundColor()); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - } - - /** - * @return the current plot as a {@link BufferedImage} - */ - public BufferedImage getImage() { - return image; - } - - /** - * open a JFrame containing the supplied {@link BufferedImage} - * - * @param image image to display - */ - public static void showJFrame(BufferedImage image) { - JFrame frame = new JFrame(); - JPanel panel = new JPanel(); - panel.add(new JLabel(new ImageIcon(image))); - - JScrollPane pane = new JScrollPane(panel); - frame.setContentPane(pane); - frame.pack(); - frame.setVisible(true); - frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - } - - /** - * open a JFrame with the supplied dimensions containing the supplied {@link BufferedImage} - * - * @param image image to display - * @param width window width - * @param height window height - */ - public static void showJFrame(BufferedImage image, int width, int height) { - BufferedImage resizedImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = resizedImg.createGraphics(); - - g2.setRenderingHint( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(image, 0, 0, width, height, null); - g2.dispose(); - - showJFrame(resizedImg); - } - - /** - * Write the image to file (format determined by extension) - * - * @param filename filename to write - * @param image image to write - */ - public static void writeImage(String filename, BufferedImage image) { - File imageFile = new File(filename); - try { - ImageIO.write(image, filename.substring(filename.lastIndexOf(".") + 1), imageFile); - } catch (IOException e) { - e.printStackTrace(); + /** + * @return width of the plot area. Does not include margins. + */ + public int getWidth() { + return width; } - } - /** - * Write the image to file (format determined by extension) - * - * @param filename filename to write - */ - public void writeImage(String filename) { - writeImage(filename, image); - } - - /** - * Write aligned text onto a graphics2D - * - * @param g Graphics2D object - * @param text text to write - * @param x X coordinate of the aligned point - * @param y Y coordinate of the aligned point - * @param vert {@link Keyword#ALIGN_TOP}, {@link Keyword#ALIGN_CENTER}, or {@link - * Keyword#ALIGN_BOTTOM} - * @param horiz {@link Keyword#ALIGN_LEFT}, {@link Keyword#ALIGN_CENTER}, or {@link - * Keyword#ALIGN_RIGHT} - */ - public void addAnnotation( - Graphics2D g, String text, double x, double y, Keyword vert, Keyword horiz) { - for (String line : text.split("\n")) { - Point2D.Double coords = StringUtils.stringCoordinates(g, line, x, y, vert, horiz); - // StringUtils.drawOutlinedString(g, line, (int) (coords.getX() + 0.5), (int) (coords.getY() + - // 0.5)); - g.drawString(line, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); - y += g.getFontMetrics().getHeight(); + /** + * @return height of the plot area. Does not include margins. + */ + public int getHeight() { + return height; } - } - /** - * @param g Graphics2D object - * @param text text to write - * @param x X coordinate of the aligned point - * @param y Y coordinate of the aligned point - * @param rotate angle to rotate text - */ - public void addAnnotation(Graphics2D g, String text, double x, double y, double rotate) { - g.translate(x, y); - g.rotate(rotate); - addAnnotation(g, text, 0, 0, Keyword.ALIGN_CENTER, Keyword.ALIGN_CENTER); - g.rotate(-rotate); - g.translate(-x, -y); - } - - /** - * @param a Annotation - * @param x x coordinate. See {@link Annotation#horizontalAlignment()} - * @param y y coordinate.See {@link Annotation#verticalAlignment()} - * @param rotate rotation in radians - */ - public void addAnnotation(Annotation a, double x, double y, double rotate) { - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - g.setColor(a.color()); - g.setFont(a.font()); - g.translate(x, y); - g.rotate(rotate); - addAnnotation(g, a.text(), 0, 0, a.verticalAlignment(), a.horizontalAlignment()); - } - - /** - * Set pixel at (x,y) to color - * - * @param x pixel x value - * @param y pixel y value - * @param color color - */ - public void setPixel(double x, double y, Color color) { - int pixelX = (int) (x + 0.5); - int pixelY = (int) (y + 0.5); - if (pixelX >= 0 && pixelX < image.getWidth() && pixelY >= 0 && pixelY < image.getHeight()) - image.setRGB(pixelX, pixelY, color.getRGB()); - } - - /** - * Set pixel at (x,y) to a shade of gray. Bilinear interpolation is used to spread the pixel over - * a 2x2 square. - * - *

    -   * x axis passes through 0 and 2
    -   * y axis passes through 0 and 1
    -   *
    -   * Given a point (x, y), compute the area weighting of each pixel.
    -   *
    -   * --- ---
    -   * | 0 | 1 |
    -   * --- ---
    -   * | 2 | 3 |
    -   * --- ---
    -   *
    -   * 
    - * - * Weights are from Numerical Recipes, 2nd Edition
    - * weight[0] = (1 - t) * u;
    - * weight[1] = t * u;
    - * weight[2] = (1-t) * (1-u);
    - * weight[3] = t * (1-u);
    - * - *

    - * - * @param x pixel x value - * @param y pixel y value - * @param brightness pixel value, must be >0, but can be more than 255 - * @param whiteBackground true if plotting against a white background (brightness of 255 maps to - * zero). If false, higher weighted square will be lighter (brightness of 255 stays 255). - */ - public void setPixel(double x, double y, int brightness, boolean whiteBackground) { - - double t = x - Math.floor(x); - double u = 1 - (y - Math.floor(y)); - double[] weights = new double[4]; - weights[1] = t * u; - weights[0] = u - weights[1]; - weights[2] = 1 - t - u + weights[1]; - weights[3] = t - weights[1]; - - Map weightMap = new HashMap<>(); - - int x0 = (int) x; - int x1 = x0 + 1; - int y0 = (int) y; - int y1 = y0 + 1; - - boolean x0Good = x0 >= 0 && x0 < image.getWidth(); - boolean x1Good = x1 >= 0 && x1 < image.getWidth(); - - boolean y0Good = y0 >= 0 && y0 < image.getHeight(); - boolean y1Good = y1 >= 0 && y1 < image.getHeight(); - - if (x0Good && y0Good) weightMap.put(new Point2D.Float(x0, y0), weights[0]); - if (x1Good && y0Good) weightMap.put(new Point2D.Float(x1, y0), weights[1]); - if (x0Good && y1Good) weightMap.put(new Point2D.Float(x0, y1), weights[2]); - if (x1Good && y1Good) weightMap.put(new Point2D.Float(x1, y1), weights[3]); - - for (Point2D pair : weightMap.keySet()) { - float b = Math.min(255, weightMap.get(pair).floatValue() * brightness); - if (whiteBackground) b = 255 - b; - Color c = Color.getHSBColor(0, 0, b / 255); - image.setRGB((int) pair.getX(), (int) pair.getY(), c.getRGB()); + /** + * @return width of the entire page, including left and right margins. + */ + public int getPageWidth() { + return pageWidth; } - } - /** - * Add an entry to the legend. - * - * @param entry Legend entry - */ - public void addToLegend(LegendEntry entry) { - legend.add(entry); - } - - public void clearLegend() { - legend = new ArrayList<>(); - } - - /** Draw the legend. */ - public void drawLegend() { - - if (!legend.isEmpty()) { - double ulx = config.legendPosition().getX(); - double boxWidth = 0; - double lry = config.legendPosition().getY(); - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setFont(config.legendFont()); - - double maxTextWidth = -Double.MAX_VALUE; - double maxSymbolWidth = -Double.MAX_VALUE; - for (LegendEntry entry : legend) { - String text = entry.name(); - lry += 1.5 * StringUtils.stringHeight(g, text); - double textWidth = StringUtils.stringWidth(g, String.format(" %s ", text)); - double symbolWidth = entry.symbol().isEmpty() ? 0 : entry.symbol().get().getSize(); - - if (entry.stroke().isPresent()) { - symbolWidth = Math.max(5, symbolWidth); - textWidth += 5; - } - - maxTextWidth = Math.max(textWidth, maxTextWidth); - maxSymbolWidth = Math.max(symbolWidth, maxSymbolWidth); - - boxWidth = Math.max(maxTextWidth + 2 * maxSymbolWidth, boxWidth); - } - - double uly = config.legendPosition().getY(); - double boxHeight = lry - uly; - double charWidth = StringUtils.stringWidth(g, "a"); - double charHeight = StringUtils.stringHeight(g, "a"); - - g.setColor(config.backgroundColor()); - g.fill( - new Rectangle2D.Double( - ulx - charWidth, - uly - charHeight, - boxWidth + 2 * charWidth, - boxHeight + 0.5 * charHeight)); - g.setColor(Color.BLACK); - g.draw( - new Rectangle2D.Double( - ulx - charWidth, - uly - charHeight, - boxWidth + 2 * charWidth, - boxHeight + 0.5 * charHeight)); - - lry = config.legendPosition().getY(); - for (LegendEntry entry : legend) { - String text = entry.name(); - g.setColor(entry.color()); - double legendX = config.legendPosition().getX(); - if (entry.symbol().isPresent()) { - Symbol symbol = entry.symbol().get(); - legendX += 0.5 * maxSymbolWidth; - symbol.draw(g, legendX, lry); - legendX += 1.5 * maxSymbolWidth; - } - if (entry.stroke().isPresent()) { - g.setStroke(entry.stroke().get()); - GeneralPath gp = new GeneralPath(); - gp.moveTo(legendX, lry); - legendX += 5; - gp.lineTo(legendX, lry); - legendX += 5; - g.setColor(entry.color()); - g.draw(gp); - } - - g.setColor(Color.BLACK); - if (config.legendOutline()) { - addAnnotation(g, text, legendX + 1, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); - addAnnotation(g, text, legendX - 1, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); - addAnnotation(g, text, legendX, lry + 1, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); - addAnnotation(g, text, legendX, lry - 1, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); - } - - if (config.legendColor()) g.setColor(entry.color()); - - addAnnotation(g, text, legendX, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); - - lry += 1.5 * StringUtils.stringHeight(g, text); - g.setColor(Color.BLACK); - } + /** + * @return height of the entire page, including top and bottom margins. + */ + public int getPageHeight() { + return pageHeight; } - } - /** - * Taken from crucible.mantle.contacts.GraphicsUtils - * - *

    Configure {@link RenderingHints} on a {@link Graphics2D} object to render with sub-pixel - * accuracy enabled. - * - *

    This method encapsulates the set of rendering hints we have found largely through trial and - * error that yield proper results across multiple platforms. Platform to platform so of the hints - * configured here may be unnecessary, but given the platform and API dependent nature of these - * values it seems an appropriate route to take. - * - * @param graphics the graphics context to adjust the rendering hints - * @return the original hints, which can be restored with {@link - * Graphics2D#setRenderingHints(Map)} - */ - public static RenderingHints configureHintsForSubpixelQuality(Graphics2D graphics) { + public PlotCanvas(PlotConfig config) { + super(); + this.config = config; + this.legend = new ArrayList<>(); - RenderingHints original = graphics.getRenderingHints(); + this.width = config.width(); + this.height = config.height(); + this.pageWidth = config.width() + config.leftMargin() + config.rightMargin(); + this.pageHeight = config.height() + config.topMargin() + config.bottomMargin(); + this.image = new BufferedImage(pageWidth, pageHeight, BufferedImage.TYPE_INT_ARGB); - /* - * Enable quality rendering hint, as that is what we are chasing. + Graphics2D g = image.createGraphics(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setComposite(AlphaComposite.Src); + g.setColor(config.backgroundColor()); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + + /** + * @return the current plot as a {@link BufferedImage} */ - graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + public BufferedImage getImage() { + return image; + } - /* - * Turn on Anti-Aliasing, as this is necessary for rendering subpixel stuff accurately and - * smoothly. + /** + * open a JFrame containing the supplied {@link BufferedImage} + * + * @param image image to display */ - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - /* - * Grant found that enabling pure stroke mode gives proper subpixel stroking, necessary for - * rendering shapes and other 2D API objects accurately. + public static void showJFrame(BufferedImage image) { + JFrame frame = new JFrame(); + JPanel panel = new JPanel(); + panel.add(new JLabel(new ImageIcon(image))); + + JScrollPane pane = new JScrollPane(panel); + frame.setContentPane(pane); + frame.pack(); + frame.setVisible(true); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } + + /** + * open a JFrame with the supplied dimensions containing the supplied {@link BufferedImage} + * + * @param image image to display + * @param width window width + * @param height window height */ - graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + public static void showJFrame(BufferedImage image, int width, int height) { + BufferedImage resizedImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = resizedImg.createGraphics(); - graphics.setRenderingHint( - RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(image, 0, 0, width, height, null); + g2.dispose(); - /* - * Turn on the alpha interpolation to quality, as we are rendering transparent images. + showJFrame(resizedImg); + } + + /** + * Write the image to file (format determined by extension) + * + * @param filename filename to write + * @param image image to write */ - graphics.setRenderingHint( - RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + public static void writeImage(String filename, BufferedImage image) { + File imageFile = new File(filename); + try { + ImageIO.write(image, filename.substring(filename.lastIndexOf(".") + 1), imageFile); + } catch (IOException e) { + e.printStackTrace(); + } + } - /* - * Improve the quality of output fonts. + /** + * Write the image to file (format determined by extension) + * + * @param filename filename to write */ - graphics.setRenderingHint( - RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); - graphics.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); + public void writeImage(String filename) { + writeImage(filename, image); + } - return original; - } + /** + * Write aligned text onto a graphics2D + * + * @param g Graphics2D object + * @param text text to write + * @param x X coordinate of the aligned point + * @param y Y coordinate of the aligned point + * @param vert {@link Keyword#ALIGN_TOP}, {@link Keyword#ALIGN_CENTER}, or {@link + * Keyword#ALIGN_BOTTOM} + * @param horiz {@link Keyword#ALIGN_LEFT}, {@link Keyword#ALIGN_CENTER}, or {@link + * Keyword#ALIGN_RIGHT} + */ + public void addAnnotation(Graphics2D g, String text, double x, double y, Keyword vert, Keyword horiz) { + for (String line : text.split("\n")) { + Point2D.Double coords = StringUtils.stringCoordinates(g, line, x, y, vert, horiz); + // StringUtils.drawOutlinedString(g, line, (int) (coords.getX() + 0.5), (int) (coords.getY() + + // 0.5)); + g.drawString(line, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); + y += g.getFontMetrics().getHeight(); + } + } - /** - * Draw a color bar. - * - * @param colorBar color bar - */ - public void drawColorBar(ColorBar colorBar) { - - Rectangle rect = colorBar.rect(); - ColorRamp ramp = colorBar.ramp(); - Function tickFunction = colorBar.tickFunction(); - int nTicks = colorBar.numTicks(); - - final boolean horizontal = rect.getWidth() > rect.getHeight(); - - Graphics2D g = getImage().createGraphics(); - configureHintsForSubpixelQuality(g); - g.setFont(config.axisFont()); - g.setColor(Color.BLACK); - g.draw(rect); - - if (horizontal) { - if (!ramp.title().isEmpty()) { - addAnnotation( - g, - ramp.title(), - rect.getWidth() / 2 + rect.getX(), - rect.y - 10, - Keyword.ALIGN_BOTTOM, - Keyword.ALIGN_CENTER); - } - - double tickSpacing = (rect.getWidth() / (nTicks - 1.)); - int pixelX; - int pixelY = rect.y + rect.height; - - for (double i = 0; i < rect.getWidth(); i += tickSpacing) { - double frac = i / rect.getWidth(); - double value = frac * (ramp.max() - ramp.min()) + ramp.min(); - if (colorBar.log()) value = Math.pow(10, value); - - pixelX = (int) (rect.x + i + 0.5); - g.drawLine(pixelX, pixelY, pixelX, pixelY + 5); - addAnnotation( - g, - tickFunction.apply(value), - pixelX, - pixelY + 10, - Keyword.ALIGN_TOP, - Keyword.ALIGN_CENTER); - } - pixelX = rect.x + rect.width; - g.drawLine(pixelX, pixelY, pixelX, pixelY + 5); - - addAnnotation( - g, - tickFunction.apply(colorBar.log() ? Math.pow(10, ramp.max()) : ramp.max()), - pixelX, - pixelY + 10, - Keyword.ALIGN_TOP, - Keyword.ALIGN_CENTER); - - for (double i = 0; i < rect.getWidth(); i++) { - double frac = i / (rect.getWidth() - 1); - double value = frac * (ramp.max() - ramp.min()) + ramp.min(); - pixelX = (int) (rect.x + i + 0.5); - Color color = ramp.getColor(value); - g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 255)); - g.drawLine(pixelX, rect.y, pixelX, rect.y + rect.height); - } - } else { - - if (!ramp.title().isEmpty()) { - g.setColor(Color.BLACK); - // rotate axis label - double x = rect.x - 10; - double y = rect.getHeight() / 2 + rect.getY(); + /** + * @param g Graphics2D object + * @param text text to write + * @param x X coordinate of the aligned point + * @param y Y coordinate of the aligned point + * @param rotate angle to rotate text + */ + public void addAnnotation(Graphics2D g, String text, double x, double y, double rotate) { g.translate(x, y); - g.rotate(-Math.PI / 2); - addAnnotation(g, ramp.title(), 0, 0, Keyword.ALIGN_BOTTOM, Keyword.ALIGN_CENTER); - g.rotate(Math.PI / 2); + g.rotate(rotate); + addAnnotation(g, text, 0, 0, Keyword.ALIGN_CENTER, Keyword.ALIGN_CENTER); + g.rotate(-rotate); g.translate(-x, -y); - } - - double tickSpacing = (rect.getHeight() / (nTicks - 1.)); - int pixelY; - int pixelX = rect.x + rect.width; - for (double i = 0; i < rect.getHeight(); i += tickSpacing) { - double frac = 1 - i / rect.getHeight(); - double value = frac * (ramp.max() - ramp.min()) + ramp.min(); - if (colorBar.log()) value = Math.pow(10, value); - - pixelY = (int) (rect.y + i + 0.5); - g.drawLine(pixelX, pixelY, pixelX + 5, pixelY); - addAnnotation( - g, - tickFunction.apply(value), - pixelX + 10, - pixelY, - Keyword.ALIGN_CENTER, - Keyword.ALIGN_LEFT); - } - pixelY = rect.y + rect.height; - g.drawLine(pixelX, pixelY, pixelX + 5, pixelY); - addAnnotation( - g, - tickFunction.apply(colorBar.log() ? Math.pow(10, ramp.min()) : ramp.min()), - pixelX + 10, - pixelY, - Keyword.ALIGN_CENTER, - Keyword.ALIGN_LEFT); - - for (double i = 0; i < rect.getHeight(); i++) { - double frac = 1 - i / (rect.getHeight() - 1); - double value = frac * (ramp.max() - ramp.min()) + ramp.min(); - pixelY = (int) (rect.y + i + 0.5); - Color color = ramp.getColor(value); - g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 255)); - g.drawLine(rect.x, pixelY, rect.x + rect.width, pixelY); - } } - } + + /** + * @param a Annotation + * @param x x coordinate. See {@link Annotation#horizontalAlignment()} + * @param y y coordinate.See {@link Annotation#verticalAlignment()} + * @param rotate rotation in radians + */ + public void addAnnotation(Annotation a, double x, double y, double rotate) { + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + g.setColor(a.color()); + g.setFont(a.font()); + g.translate(x, y); + g.rotate(rotate); + addAnnotation(g, a.text(), 0, 0, a.verticalAlignment(), a.horizontalAlignment()); + } + + /** + * Set pixel at (x,y) to color + * + * @param x pixel x value + * @param y pixel y value + * @param color color + */ + public void setPixel(double x, double y, Color color) { + int pixelX = (int) (x + 0.5); + int pixelY = (int) (y + 0.5); + if (pixelX >= 0 && pixelX < image.getWidth() && pixelY >= 0 && pixelY < image.getHeight()) + image.setRGB(pixelX, pixelY, color.getRGB()); + } + + /** + * Set pixel at (x,y) to a shade of gray. Bilinear interpolation is used to spread the pixel over + * a 2x2 square. + * + *

    +     * x axis passes through 0 and 2
    +     * y axis passes through 0 and 1
    +     *
    +     * Given a point (x, y), compute the area weighting of each pixel.
    +     *
    +     * --- ---
    +     * | 0 | 1 |
    +     * --- ---
    +     * | 2 | 3 |
    +     * --- ---
    +     *
    +     * 
    + * + * Weights are from Numerical Recipes, 2nd Edition
    + * weight[0] = (1 - t) * u;
    + * weight[1] = t * u;
    + * weight[2] = (1-t) * (1-u);
    + * weight[3] = t * (1-u);
    + * + *

    + * + * @param x pixel x value + * @param y pixel y value + * @param brightness pixel value, must be >0, but can be more than 255 + * @param whiteBackground true if plotting against a white background (brightness of 255 maps to + * zero). If false, higher weighted square will be lighter (brightness of 255 stays 255). + */ + public void setPixel(double x, double y, int brightness, boolean whiteBackground) { + + double t = x - Math.floor(x); + double u = 1 - (y - Math.floor(y)); + double[] weights = new double[4]; + weights[1] = t * u; + weights[0] = u - weights[1]; + weights[2] = 1 - t - u + weights[1]; + weights[3] = t - weights[1]; + + Map weightMap = new HashMap<>(); + + int x0 = (int) x; + int x1 = x0 + 1; + int y0 = (int) y; + int y1 = y0 + 1; + + boolean x0Good = x0 >= 0 && x0 < image.getWidth(); + boolean x1Good = x1 >= 0 && x1 < image.getWidth(); + + boolean y0Good = y0 >= 0 && y0 < image.getHeight(); + boolean y1Good = y1 >= 0 && y1 < image.getHeight(); + + if (x0Good && y0Good) weightMap.put(new Point2D.Float(x0, y0), weights[0]); + if (x1Good && y0Good) weightMap.put(new Point2D.Float(x1, y0), weights[1]); + if (x0Good && y1Good) weightMap.put(new Point2D.Float(x0, y1), weights[2]); + if (x1Good && y1Good) weightMap.put(new Point2D.Float(x1, y1), weights[3]); + + for (Point2D pair : weightMap.keySet()) { + float b = Math.min(255, weightMap.get(pair).floatValue() * brightness); + if (whiteBackground) b = 255 - b; + Color c = Color.getHSBColor(0, 0, b / 255); + image.setRGB((int) pair.getX(), (int) pair.getY(), c.getRGB()); + } + } + + /** + * Add an entry to the legend. + * + * @param entry Legend entry + */ + public void addToLegend(LegendEntry entry) { + legend.add(entry); + } + + public void clearLegend() { + legend = new ArrayList<>(); + } + + /** Draw the legend. */ + public void drawLegend() { + + if (!legend.isEmpty()) { + double ulx = config.legendPosition().getX(); + double boxWidth = 0; + double lry = config.legendPosition().getY(); + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setFont(config.legendFont()); + + double maxTextWidth = -Double.MAX_VALUE; + double maxSymbolWidth = -Double.MAX_VALUE; + for (LegendEntry entry : legend) { + String text = entry.name(); + lry += 1.5 * StringUtils.stringHeight(g, text); + double textWidth = StringUtils.stringWidth(g, String.format(" %s ", text)); + double symbolWidth = + entry.symbol().isEmpty() ? 0 : entry.symbol().get().getSize(); + + if (entry.stroke().isPresent()) { + symbolWidth = Math.max(5, symbolWidth); + textWidth += 5; + } + + maxTextWidth = Math.max(textWidth, maxTextWidth); + maxSymbolWidth = Math.max(symbolWidth, maxSymbolWidth); + + boxWidth = Math.max(maxTextWidth + 2 * maxSymbolWidth, boxWidth); + } + + double uly = config.legendPosition().getY(); + double boxHeight = lry - uly; + double charWidth = StringUtils.stringWidth(g, "a"); + double charHeight = StringUtils.stringHeight(g, "a"); + + g.setColor(config.backgroundColor()); + g.fill(new Rectangle2D.Double( + ulx - charWidth, uly - charHeight, boxWidth + 2 * charWidth, boxHeight + 0.5 * charHeight)); + g.setColor(Color.BLACK); + g.draw(new Rectangle2D.Double( + ulx - charWidth, uly - charHeight, boxWidth + 2 * charWidth, boxHeight + 0.5 * charHeight)); + + lry = config.legendPosition().getY(); + for (LegendEntry entry : legend) { + String text = entry.name(); + g.setColor(entry.color()); + double legendX = config.legendPosition().getX(); + if (entry.symbol().isPresent()) { + Symbol symbol = entry.symbol().get(); + legendX += 0.5 * maxSymbolWidth; + symbol.draw(g, legendX, lry); + legendX += 1.5 * maxSymbolWidth; + } + if (entry.stroke().isPresent()) { + g.setStroke(entry.stroke().get()); + GeneralPath gp = new GeneralPath(); + gp.moveTo(legendX, lry); + legendX += 5; + gp.lineTo(legendX, lry); + legendX += 5; + g.setColor(entry.color()); + g.draw(gp); + } + + g.setColor(Color.BLACK); + if (config.legendOutline()) { + addAnnotation(g, text, legendX + 1, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + addAnnotation(g, text, legendX - 1, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + addAnnotation(g, text, legendX, lry + 1, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + addAnnotation(g, text, legendX, lry - 1, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + } + + if (config.legendColor()) g.setColor(entry.color()); + + addAnnotation(g, text, legendX, lry, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + + lry += 1.5 * StringUtils.stringHeight(g, text); + g.setColor(Color.BLACK); + } + } + } + + /** + * Taken from crucible.mantle.contacts.GraphicsUtils + * + *

    Configure {@link RenderingHints} on a {@link Graphics2D} object to render with sub-pixel + * accuracy enabled. + * + *

    This method encapsulates the set of rendering hints we have found largely through trial and + * error that yield proper results across multiple platforms. Platform to platform so of the hints + * configured here may be unnecessary, but given the platform and API dependent nature of these + * values it seems an appropriate route to take. + * + * @param graphics the graphics context to adjust the rendering hints + * @return the original hints, which can be restored with {@link + * Graphics2D#setRenderingHints(Map)} + */ + public static RenderingHints configureHintsForSubpixelQuality(Graphics2D graphics) { + + RenderingHints original = graphics.getRenderingHints(); + + /* + * Enable quality rendering hint, as that is what we are chasing. + */ + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + /* + * Turn on Anti-Aliasing, as this is necessary for rendering subpixel stuff accurately and + * smoothly. + */ + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + /* + * Grant found that enabling pure stroke mode gives proper subpixel stroking, necessary for + * rendering shapes and other 2D API objects accurately. + */ + graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + + /* + * Turn on the alpha interpolation to quality, as we are rendering transparent images. + */ + graphics.setRenderingHint( + RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + + /* + * Improve the quality of output fonts. + */ + graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); + + return original; + } + + /** + * Draw a color bar. + * + * @param colorBar color bar + */ + public void drawColorBar(ColorBar colorBar) { + + Rectangle rect = colorBar.rect(); + ColorRamp ramp = colorBar.ramp(); + Function tickFunction = colorBar.tickFunction(); + int nTicks = colorBar.numTicks(); + + final boolean horizontal = rect.getWidth() > rect.getHeight(); + + Graphics2D g = getImage().createGraphics(); + configureHintsForSubpixelQuality(g); + g.setFont(config.axisFont()); + g.setColor(Color.BLACK); + g.draw(rect); + + if (horizontal) { + if (!ramp.title().isEmpty()) { + addAnnotation( + g, + ramp.title(), + rect.getWidth() / 2 + rect.getX(), + rect.y - 10, + Keyword.ALIGN_BOTTOM, + Keyword.ALIGN_CENTER); + } + + double tickSpacing = (rect.getWidth() / (nTicks - 1.)); + int pixelX; + int pixelY = rect.y + rect.height; + + for (double i = 0; i < rect.getWidth(); i += tickSpacing) { + double frac = i / rect.getWidth(); + double value = frac * (ramp.max() - ramp.min()) + ramp.min(); + if (colorBar.log()) value = Math.pow(10, value); + + pixelX = (int) (rect.x + i + 0.5); + g.drawLine(pixelX, pixelY, pixelX, pixelY + 5); + addAnnotation( + g, tickFunction.apply(value), pixelX, pixelY + 10, Keyword.ALIGN_TOP, Keyword.ALIGN_CENTER); + } + pixelX = rect.x + rect.width; + g.drawLine(pixelX, pixelY, pixelX, pixelY + 5); + + addAnnotation( + g, + tickFunction.apply(colorBar.log() ? Math.pow(10, ramp.max()) : ramp.max()), + pixelX, + pixelY + 10, + Keyword.ALIGN_TOP, + Keyword.ALIGN_CENTER); + + for (double i = 0; i < rect.getWidth(); i++) { + double frac = i / (rect.getWidth() - 1); + double value = frac * (ramp.max() - ramp.min()) + ramp.min(); + pixelX = (int) (rect.x + i + 0.5); + Color color = ramp.getColor(value); + g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 255)); + g.drawLine(pixelX, rect.y, pixelX, rect.y + rect.height); + } + } else { + + if (!ramp.title().isEmpty()) { + g.setColor(Color.BLACK); + // rotate axis label + double x = rect.x - 10; + double y = rect.getHeight() / 2 + rect.getY(); + g.translate(x, y); + g.rotate(-Math.PI / 2); + addAnnotation(g, ramp.title(), 0, 0, Keyword.ALIGN_BOTTOM, Keyword.ALIGN_CENTER); + g.rotate(Math.PI / 2); + g.translate(-x, -y); + } + + double tickSpacing = (rect.getHeight() / (nTicks - 1.)); + int pixelY; + int pixelX = rect.x + rect.width; + for (double i = 0; i < rect.getHeight(); i += tickSpacing) { + double frac = 1 - i / rect.getHeight(); + double value = frac * (ramp.max() - ramp.min()) + ramp.min(); + if (colorBar.log()) value = Math.pow(10, value); + + pixelY = (int) (rect.y + i + 0.5); + g.drawLine(pixelX, pixelY, pixelX + 5, pixelY); + addAnnotation( + g, tickFunction.apply(value), pixelX + 10, pixelY, Keyword.ALIGN_CENTER, Keyword.ALIGN_LEFT); + } + pixelY = rect.y + rect.height; + g.drawLine(pixelX, pixelY, pixelX + 5, pixelY); + addAnnotation( + g, + tickFunction.apply(colorBar.log() ? Math.pow(10, ramp.min()) : ramp.min()), + pixelX + 10, + pixelY, + Keyword.ALIGN_CENTER, + Keyword.ALIGN_LEFT); + + for (double i = 0; i < rect.getHeight(); i++) { + double frac = 1 - i / (rect.getHeight() - 1); + double value = frac * (ramp.max() - ramp.min()) + ramp.min(); + pixelY = (int) (rect.y + i + 0.5); + Color color = ramp.getColor(value); + g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 255)); + g.drawLine(rect.x, pixelY, rect.x + rect.width, pixelY); + } + } + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/PolarPlot.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/PolarPlot.java index 19295d4..f4087ce 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/PolarPlot.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/PolarPlot.java @@ -43,423 +43,416 @@ import terrasaur.utils.saaPlotLib.util.Keyword; /** * 0,0 is at top left - * + * * @author nairah1 * */ public class PolarPlot extends PlotCanvas { - protected final AxisR axisR; - protected final AxisTheta axisTheta; - final protected int radius; - final protected Point2D origin; - private double rotate; + protected final AxisR axisR; + protected final AxisTheta axisTheta; + protected final int radius; + protected final Point2D origin; + private double rotate; - public PolarPlot(PlotConfig config, AxisR axisR, AxisTheta axisTheta) { - super(config); + public PolarPlot(PlotConfig config, AxisR axisR, AxisTheta axisTheta) { + super(config); - this.axisR = axisR; - this.axisTheta = axisTheta; + this.axisR = axisR; + this.axisTheta = axisTheta; - radius = Math.min(getWidth(), getHeight()) / 2; - origin = new Point2D.Double(config.leftMargin() + getWidth() / 2., - config.topMargin() + getHeight() / 2.); + radius = Math.min(getWidth(), getHeight()) / 2; + origin = new Point2D.Double(config.leftMargin() + getWidth() / 2., config.topMargin() + getHeight() / 2.); - rotate = 0; - } - - public PolarPlot setRotate(double rotate) { - this.rotate = rotate; - return this; - } - - private Graphics2D getGraphics(boolean applyTransform) { - Graphics2D g = image.createGraphics(); - configureHintsForSubpixelQuality(g); - if (applyTransform) { - AffineTransform transform = - AffineTransform.getRotateInstance(-rotate, origin.getX(), origin.getY()); - g.setTransform(transform); - } - return g; - } - - /** - * - * @param a Annotation - * @param x pixel x coordinate - * @param y pixel y coordinate - * @param rotate rotation angle in radians - */ - @Override - public void addAnnotation(Annotation a, double x, double y, double rotate) { - Graphics2D g = getGraphics(true); - g.setColor(a.color()); - g.setFont(a.font()); - g.translate(x, y); - g.rotate(rotate); - addAnnotation(g, a.text(), 0, 0, a.verticalAlignment(), a.horizontalAlignment()); - } - - @Override - public void setPixel(double x, double y, int brightness, boolean whiteBackground) { - - Point2D src = new Point2D.Double(x, y); - Point2D dst = getGraphics(true).getTransform().transform(src, new Point2D.Double()); - - super.setPixel(dst.getX(), dst.getY(), brightness, whiteBackground); - } - - /** - * Draw tick marks and labels - */ - public void drawAxes() { - Graphics2D g = getGraphics(true); - g.setFont(config.axisFont()); - - // draw theta - g.setColor(axisTheta.getAxisColor()); - g.draw(getBoundary()); - - double majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); - double minorTickLength = 0.5 * majorTickLength; - - NavigableSet minorTicks = axisTheta.getMinorTicks(); - if (minorTicks != null) { - for (double minorTick : minorTicks) { - if (!axisTheta.getRange().closedContains(minorTick)) - continue; - - Path2D.Double path = new Path2D.Double(); - path.moveTo(origin.getX() + (radius + minorTickLength / 2) * Math.cos(minorTick), - origin.getY() - (radius + minorTickLength / 2) * Math.sin(minorTick)); - path.lineTo(origin.getX() + (radius - minorTickLength / 2) * Math.cos(minorTick), - origin.getY() - (radius - minorTickLength / 2) * Math.sin(minorTick)); - g.draw(path); - - } + rotate = 0; } - NavigableMap tickLabels = axisTheta.getTickLabels(); - for (double majorTick : tickLabels.keySet()) { - if (!axisTheta.getRange().closedContains(majorTick)) - continue; - Path2D.Double path = new Path2D.Double(); - path.moveTo(origin.getX() + (radius + minorTickLength / 2) * Math.cos(majorTick), - origin.getY() - (radius + minorTickLength / 2) * Math.sin(majorTick)); - path.lineTo(origin.getX() + (radius - minorTickLength / 2) * Math.cos(majorTick), - origin.getY() - (radius - minorTickLength / 2) * Math.sin(majorTick)); - g.draw(path); + public PolarPlot setRotate(double rotate) { + this.rotate = rotate; + return this; + } - if (!axisTheta.getTitle().isEmpty()) { - boolean printThis = true; - if (majorTick == tickLabels.navigableKeySet().last()) { + private Graphics2D getGraphics(boolean applyTransform) { + Graphics2D g = image.createGraphics(); + configureHintsForSubpixelQuality(g); + if (applyTransform) { + AffineTransform transform = AffineTransform.getRotateInstance(-rotate, origin.getX(), origin.getY()); + g.setTransform(transform); + } + return g; + } - if (Math - .abs(Math.sin(tickLabels.navigableKeySet().first()) - - Math.sin(tickLabels.navigableKeySet().last())) < 1e-8 - && Math.abs(Math.cos(tickLabels.navigableKeySet().first()) - - Math.cos(tickLabels.navigableKeySet().last())) < 1e-8) { - printThis = false; - } + /** + * + * @param a Annotation + * @param x pixel x coordinate + * @param y pixel y coordinate + * @param rotate rotation angle in radians + */ + @Override + public void addAnnotation(Annotation a, double x, double y, double rotate) { + Graphics2D g = getGraphics(true); + g.setColor(a.color()); + g.setFont(a.font()); + g.translate(x, y); + g.rotate(rotate); + addAnnotation(g, a.text(), 0, 0, a.verticalAlignment(), a.horizontalAlignment()); + } + + @Override + public void setPixel(double x, double y, int brightness, boolean whiteBackground) { + + Point2D src = new Point2D.Double(x, y); + Point2D dst = getGraphics(true).getTransform().transform(src, new Point2D.Double()); + + super.setPixel(dst.getX(), dst.getY(), brightness, whiteBackground); + } + + /** + * Draw tick marks and labels + */ + public void drawAxes() { + Graphics2D g = getGraphics(true); + g.setFont(config.axisFont()); + + // draw theta + g.setColor(axisTheta.getAxisColor()); + g.draw(getBoundary()); + + double majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); + double minorTickLength = 0.5 * majorTickLength; + + NavigableSet minorTicks = axisTheta.getMinorTicks(); + if (minorTicks != null) { + for (double minorTick : minorTicks) { + if (!axisTheta.getRange().closedContains(minorTick)) continue; + + Path2D.Double path = new Path2D.Double(); + path.moveTo( + origin.getX() + (radius + minorTickLength / 2) * Math.cos(minorTick), + origin.getY() - (radius + minorTickLength / 2) * Math.sin(minorTick)); + path.lineTo( + origin.getX() + (radius - minorTickLength / 2) * Math.cos(minorTick), + origin.getY() - (radius - minorTickLength / 2) * Math.sin(minorTick)); + g.draw(path); + } } - if (printThis) { - double pointX = origin.getX() + (radius + majorTickLength) * Math.cos(majorTick); - double pointY = origin.getY() - (radius + majorTickLength) * Math.sin(majorTick); - addAnnotation(g, tickLabels.get(majorTick), pointX, pointY, rotate); + NavigableMap tickLabels = axisTheta.getTickLabels(); + for (double majorTick : tickLabels.keySet()) { + if (!axisTheta.getRange().closedContains(majorTick)) continue; + Path2D.Double path = new Path2D.Double(); + path.moveTo( + origin.getX() + (radius + minorTickLength / 2) * Math.cos(majorTick), + origin.getY() - (radius + minorTickLength / 2) * Math.sin(majorTick)); + path.lineTo( + origin.getX() + (radius - minorTickLength / 2) * Math.cos(majorTick), + origin.getY() - (radius - minorTickLength / 2) * Math.sin(majorTick)); + g.draw(path); + + if (!axisTheta.getTitle().isEmpty()) { + boolean printThis = true; + if (majorTick == tickLabels.navigableKeySet().last()) { + + if (Math.abs(Math.sin(tickLabels.navigableKeySet().first()) + - Math.sin( + tickLabels.navigableKeySet().last())) + < 1e-8 + && Math.abs(Math.cos(tickLabels.navigableKeySet().first()) + - Math.cos( + tickLabels.navigableKeySet().last())) + < 1e-8) { + printThis = false; + } + } + + if (printThis) { + double pointX = origin.getX() + (radius + majorTickLength) * Math.cos(majorTick); + double pointY = origin.getY() - (radius + majorTickLength) * Math.sin(majorTick); + addAnnotation(g, tickLabels.get(majorTick), pointX, pointY, rotate); + } + } + } + + // draw R + g.setColor(axisR.getAxisColor()); + Point2D axisRLow = dataToPixel(axisR, axisR.getRange().getBegin(), 0); + Point2D axisRHigh = dataToPixel(axisR, axisR.getRange().getEnd(), 0); + + // Draw the R axis + if (!axisR.getTitle().isEmpty()) { + g.drawLine( + (int) Math.round(axisRLow.getX()), + (int) Math.round(origin.getY()), + (int) Math.round(axisRHigh.getX()), + (int) Math.round(origin.getY())); + } + + majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); + minorTickLength = 0.5 * majorTickLength; + minorTicks = axisR.getMinorTicks(); + if (minorTicks != null) { + for (double minorTick : minorTicks) { + if (!axisR.getRange().closedContains(minorTick)) continue; + + Path2D.Double path = new Path2D.Double(); + Point2D point = dataToPixel(axisR, minorTick, 0); + path.moveTo(point.getX(), point.getY() + minorTickLength / 2); + path.lineTo(point.getX(), point.getY() - minorTickLength / 2); + g.draw(path); + } + } + + tickLabels = axisR.getTickLabels(); + for (double majorTick : tickLabels.keySet()) { + if (!axisR.getRange().closedContains(majorTick)) continue; + + Path2D.Double path = new Path2D.Double(); + Point2D point = dataToPixel(axisR, majorTick, 0); + path.moveTo(point.getX(), point.getY() + majorTickLength / 2); + path.lineTo(point.getX(), point.getY() - majorTickLength / 2); + + g.draw(path); + + if (!axisR.getTitle().isEmpty()) { + addAnnotation(g, tickLabels.get(majorTick), point.getX(), origin.getY() + majorTickLength, rotate); + } } - } } - // draw R - g.setColor(axisR.getAxisColor()); - Point2D axisRLow = dataToPixel(axisR, axisR.getRange().getBegin(), 0); - Point2D axisRHigh = dataToPixel(axisR, axisR.getRange().getEnd(), 0); + /** + * Draw contours for the major tick marks on the R and Theta axes + */ + public void drawGrid() { + Graphics2D g = getGraphics(true); + g.setFont(config.axisFont()); + g.setColor(config.gridColor()); - // Draw the R axis - if (!axisR.getTitle().isEmpty()) { - g.drawLine((int) Math.round(axisRLow.getX()), (int) Math.round(origin.getY()), - (int) Math.round(axisRHigh.getX()), (int) Math.round(origin.getY())); + drawRGrid(g); + drawThetaGrid(g); } - majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); - minorTickLength = 0.5 * majorTickLength; - minorTicks = axisR.getMinorTicks(); - if (minorTicks != null) { - for (double minorTick : minorTicks) { - if (!axisR.getRange().closedContains(minorTick)) - continue; + /* + * Draw concentric circles at the major tick marks for the R axis + */ + private void drawRGrid(Graphics2D g) { - Path2D.Double path = new Path2D.Double(); - Point2D point = dataToPixel(axisR, minorTick, 0); - path.moveTo(point.getX(), point.getY() + minorTickLength / 2); - path.lineTo(point.getX(), point.getY() - minorTickLength / 2); - g.draw(path); - } + final double majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); + final double minorTickLength = 0.5 * majorTickLength; + NavigableSet minorTicks = axisR.getMinorTicks(); + if (minorTicks != null) { + for (double minorTick : minorTicks) { + if (!axisR.getRange().closedContains(minorTick)) continue; + + Path2D.Double path = new Path2D.Double(); + Point2D point = dataToPixel(axisR, minorTick, 0); + path.moveTo(point.getX(), point.getY() + minorTickLength / 2); + path.lineTo(point.getX(), point.getY() - minorTickLength / 2); + g.draw(path); + } + } + + NavigableMap tickLabels = axisR.getTickLabels(); + for (double majorTick : tickLabels.keySet()) { + if (!axisR.getRange().closedContains(majorTick)) continue; + + Path2D.Double path = new Path2D.Double(); + Point2D point = dataToPixel(axisR, majorTick, 0); + path.moveTo(point.getX(), point.getY() + majorTickLength / 2); + path.lineTo(point.getX(), point.getY() - majorTickLength / 2); + + g.draw(path); + + double radius = Math.abs(point.getX() - origin.getX()); + + Ellipse2D.Double circle = + new Ellipse2D.Double(origin.getX() - radius, origin.getY() - radius, 2 * radius, 2 * radius); + g.draw(circle); + } } - tickLabels = axisR.getTickLabels(); - for (double majorTick : tickLabels.keySet()) { - if (!axisR.getRange().closedContains(majorTick)) - continue; + private Arc2D.Double getBoundary() { + Point2D axisRLow = dataToPixel(axisR, axisR.getRange().getBegin(), 0); + Point2D axisRHigh = dataToPixel(axisR, axisR.getRange().getEnd(), 0); - Path2D.Double path = new Path2D.Double(); - Point2D point = dataToPixel(axisR, majorTick, 0); - path.moveTo(point.getX(), point.getY() + majorTickLength / 2); - path.lineTo(point.getX(), point.getY() - majorTickLength / 2); - - g.draw(path); - - if (!axisR.getTitle().isEmpty()) { - addAnnotation(g, tickLabels.get(majorTick), point.getX(), origin.getY() + majorTickLength, - rotate); - } + // Draw the theta boundary + double radius = Math.max(Math.abs(origin.getX() - axisRLow.getX()), Math.abs(origin.getX() - axisRHigh.getX())); + return new Arc2D.Double( + origin.getX() - radius, + origin.getY() - radius, + 2 * radius, + 2 * radius, + Math.toDegrees(axisTheta.getRange().getBegin()), + Math.toDegrees(axisTheta.getRange().getEnd()), + Arc2D.OPEN); } + /** + * Draw radial lines at each major tick mark on the theta axis + * + * @param g graphics object + */ + private void drawThetaGrid(Graphics2D g) { - } - - /** - * Draw contours for the major tick marks on the R and Theta axes - */ - public void drawGrid() { - Graphics2D g = getGraphics(true); - g.setFont(config.axisFont()); - g.setColor(config.gridColor()); - - drawRGrid(g); - drawThetaGrid(g); - } - - /* - * Draw concentric circles at the major tick marks for the R axis - */ - private void drawRGrid(Graphics2D g) { - - final double majorTickLength = 0.2 * (config.topMargin() + config.bottomMargin()); - final double minorTickLength = 0.5 * majorTickLength; - NavigableSet minorTicks = axisR.getMinorTicks(); - if (minorTicks != null) { - for (double minorTick : minorTicks) { - if (!axisR.getRange().closedContains(minorTick)) - continue; - - Path2D.Double path = new Path2D.Double(); - Point2D point = dataToPixel(axisR, minorTick, 0); - path.moveTo(point.getX(), point.getY() + minorTickLength / 2); - path.lineTo(point.getX(), point.getY() - minorTickLength / 2); - g.draw(path); - } + NavigableMap tickLabels = axisTheta.getTickLabels(); + for (double majorTick : tickLabels.keySet()) { + if (!axisTheta.getRange().closedContains(majorTick)) continue; + Path2D.Double path = new Path2D.Double(); + path.moveTo(origin.getX(), origin.getY()); + path.lineTo(origin.getX() + radius * Math.cos(majorTick), origin.getY() - radius * Math.sin(majorTick)); + g.draw(path); + } } - NavigableMap tickLabels = axisR.getTickLabels(); - for (double majorTick : tickLabels.keySet()) { - if (!axisR.getRange().closedContains(majorTick)) - continue; + public void drawTitle() { + Graphics2D g = getGraphics(false); + g.setFont(config.axisFont()); + g.setColor(Color.BLACK); - Path2D.Double path = new Path2D.Double(); - Point2D point = dataToPixel(axisR, majorTick, 0); - path.moveTo(point.getX(), point.getY() + majorTickLength / 2); - path.lineTo(point.getX(), point.getY() - majorTickLength / 2); + if (!config.title().isEmpty()) { + int x = config.leftMargin() + config.width() / 2; - g.draw(path); - - double radius = Math.abs(point.getX() - origin.getX()); - - Ellipse2D.Double circle = new Ellipse2D.Double(origin.getX() - radius, origin.getY() - radius, - 2 * radius, 2 * radius); - g.draw(circle); - } - } - - private Arc2D.Double getBoundary() { - Point2D axisRLow = dataToPixel(axisR, axisR.getRange().getBegin(), 0); - Point2D axisRHigh = dataToPixel(axisR, axisR.getRange().getEnd(), 0); - - // Draw the theta boundary - double radius = Math.max(Math.abs(origin.getX() - axisRLow.getX()), - Math.abs(origin.getX() - axisRHigh.getX())); - return new Arc2D.Double(origin.getX() - radius, origin.getY() - radius, 2 * radius, 2 * radius, - Math.toDegrees(axisTheta.getRange().getBegin()), - Math.toDegrees(axisTheta.getRange().getEnd()), Arc2D.OPEN); - } - - /** - * Draw radial lines at each major tick mark on the theta axis - * - * @param g graphics object - */ - private void drawThetaGrid(Graphics2D g) { - - NavigableMap tickLabels = axisTheta.getTickLabels(); - for (double majorTick : tickLabels.keySet()) { - if (!axisTheta.getRange().closedContains(majorTick)) - continue; - Path2D.Double path = new Path2D.Double(); - path.moveTo(origin.getX(), origin.getY()); - path.lineTo(origin.getX() + radius * Math.cos(majorTick), - origin.getY() - radius * Math.sin(majorTick)); - g.draw(path); - } - } - - public void drawTitle() { - Graphics2D g = getGraphics(false); - g.setFont(config.axisFont()); - g.setColor(Color.BLACK); - - if (!config.title().isEmpty()) { - int x = config.leftMargin() + config.width() / 2; - - g.setFont(config.titleFont()); - g.setColor(Color.BLACK); - addAnnotation(g, config.title(), x, config.topMargin() * 0.2, Keyword.ALIGN_CENTER, - Keyword.ALIGN_CENTER); - } - } - - /** - * Plots the DataSet ds using the supplied axes - * - * @param ds data set to plot - */ - public void plot(DiscreteDataSet ds) { - switch (ds.getPlotType()) { - case LINE: - plotLine(ds); - break; - case SYMBOL: - plotSymbol(ds); - break; - default: - } - } - - private void plotLine(DiscreteDataSet ds) { - PointList xy = ds.getData(); - GeneralPath gp = new GeneralPath(); - Point4D prior = xy.getFirst(); - Point2D pixel = dataToPixel(axisR, prior.getX(), prior.getY()); - gp.moveTo(pixel.getX(), pixel.getY()); - for (int i = 1; i < xy.size() - 1; i++) { - Point4D p = xy.get(i); - pixel = dataToPixel(axisR, p.getX(), p.getY()); - gp.lineTo(pixel.getX(), pixel.getY()); - } - Point4D next = xy.getLast(); - pixel = dataToPixel(axisR, next.getX(), next.getY()); - gp.lineTo(pixel.getX(), pixel.getY()); - - Graphics2D g = getGraphics(true); - g.setClip(getBoundary()); - if (ds.getStroke() != null) - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - g.draw(gp); - } - - private void plotSymbol(DiscreteDataSet ds) { - PointList xy = ds.getData(); - - Graphics2D g = getGraphics(true); - g.setClip(getBoundary()); - if (ds.getStroke() != null) - g.setStroke(ds.getStroke()); - g.setColor(ds.getColor()); - - Point4D prior = xy.getFirst(); - Point2D pixel = dataToPixel(axisR, prior.getX(), prior.getY()); - - if (ds.getColorRamp() != null) { - if (!Double.isNaN(prior.getW())) - g.setColor(ds.getColorRamp().getColor(prior.getW())); - } - ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); - /*- - Point2D xError = prior.getXError(); - xError = new Point2D.Double(dataXtoPixel(xAxis, prior.getX() - xError.getX()), - dataXtoPixel(xAxis, prior.getX() + xError.getY())); - - Point2D yError = prior.getYError(); - yError = new Point2D.Double(dataYtoPixel(yAxis, prior.getY() - yError.getX()), - dataYtoPixel(yAxis, prior.getY() + yError.getY())); - - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); - */ - for (int i = 1; i < xy.size() - 1; i++) { - Point4D p = xy.get(i); - pixel = dataToPixel(axisR, p.getX(), p.getY()); - - if (ds.getColorRamp() != null) { - if (!Double.isNaN(p.getW())) - g.setColor(ds.getColorRamp().getColor(p.getW())); - } - - ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); - /*- - xError = p.getXError(); - xError = new Point2D.Double(dataXtoPixel(xAxis, p.getX() - xError.getX()), - dataXtoPixel(xAxis, p.getX() + xError.getY())); - - yError = p.getYError(); - yError = new Point2D.Double(dataYtoPixel(yAxis, p.getY() - yError.getX()), - dataYtoPixel(yAxis, p.getY() + yError.getY())); - - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); - */ + g.setFont(config.titleFont()); + g.setColor(Color.BLACK); + addAnnotation(g, config.title(), x, config.topMargin() * 0.2, Keyword.ALIGN_CENTER, Keyword.ALIGN_CENTER); + } } - Point4D next = xy.getLast(); - pixel = dataToPixel(axisR, next.getX(), next.getY()); - - if (ds.getColorRamp() != null) { - if (!Double.isNaN(next.getW())) - g.setColor(ds.getColorRamp().getColor(next.getW())); + /** + * Plots the DataSet ds using the supplied axes + * + * @param ds data set to plot + */ + public void plot(DiscreteDataSet ds) { + switch (ds.getPlotType()) { + case LINE: + plotLine(ds); + break; + case SYMBOL: + plotSymbol(ds); + break; + default: + } } - ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); - /*- - xError = next.getXError(); - if (xError != null) { - xError = new Point2D.Double(dataXtoPixel(xAxis, next.getX() - xError.getX()), - dataXtoPixel(xAxis, next.getX() + xError.getY())); + private void plotLine(DiscreteDataSet ds) { + PointList xy = ds.getData(); + GeneralPath gp = new GeneralPath(); + Point4D prior = xy.getFirst(); + Point2D pixel = dataToPixel(axisR, prior.getX(), prior.getY()); + gp.moveTo(pixel.getX(), pixel.getY()); + for (int i = 1; i < xy.size() - 1; i++) { + Point4D p = xy.get(i); + pixel = dataToPixel(axisR, p.getX(), p.getY()); + gp.lineTo(pixel.getX(), pixel.getY()); + } + Point4D next = xy.getLast(); + pixel = dataToPixel(axisR, next.getX(), next.getY()); + gp.lineTo(pixel.getX(), pixel.getY()); + + Graphics2D g = getGraphics(true); + g.setClip(getBoundary()); + if (ds.getStroke() != null) g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + g.draw(gp); } - yError = next.getYError(); - if (yError != null) { - yError = new Point2D.Double(dataYtoPixel(yAxis, next.getY() - yError.getX()), - dataYtoPixel(yAxis, next.getY() + yError.getY())); + + private void plotSymbol(DiscreteDataSet ds) { + PointList xy = ds.getData(); + + Graphics2D g = getGraphics(true); + g.setClip(getBoundary()); + if (ds.getStroke() != null) g.setStroke(ds.getStroke()); + g.setColor(ds.getColor()); + + Point4D prior = xy.getFirst(); + Point2D pixel = dataToPixel(axisR, prior.getX(), prior.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(prior.getW())) g.setColor(ds.getColorRamp().getColor(prior.getW())); + } + ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); + /*- + Point2D xError = prior.getXError(); + xError = new Point2D.Double(dataXtoPixel(xAxis, prior.getX() - xError.getX()), + dataXtoPixel(xAxis, prior.getX() + xError.getY())); + + Point2D yError = prior.getYError(); + yError = new Point2D.Double(dataYtoPixel(yAxis, prior.getY() - yError.getX()), + dataYtoPixel(yAxis, prior.getY() + yError.getY())); + + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + */ + for (int i = 1; i < xy.size() - 1; i++) { + Point4D p = xy.get(i); + pixel = dataToPixel(axisR, p.getX(), p.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(p.getW())) g.setColor(ds.getColorRamp().getColor(p.getW())); + } + + ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); + /*- + xError = p.getXError(); + xError = new Point2D.Double(dataXtoPixel(xAxis, p.getX() - xError.getX()), + dataXtoPixel(xAxis, p.getX() + xError.getY())); + + yError = p.getYError(); + yError = new Point2D.Double(dataYtoPixel(yAxis, p.getY() - yError.getX()), + dataYtoPixel(yAxis, p.getY() + yError.getY())); + + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + */ + } + + Point4D next = xy.getLast(); + pixel = dataToPixel(axisR, next.getX(), next.getY()); + + if (ds.getColorRamp() != null) { + if (!Double.isNaN(next.getW())) g.setColor(ds.getColorRamp().getColor(next.getW())); + } + + ds.getSymbol().draw(g, pixel.getX(), pixel.getY()); + /*- + xError = next.getXError(); + if (xError != null) { + xError = new Point2D.Double(dataXtoPixel(xAxis, next.getX() - xError.getX()), + dataXtoPixel(xAxis, next.getX() + xError.getY())); + } + yError = next.getYError(); + if (yError != null) { + yError = new Point2D.Double(dataYtoPixel(yAxis, next.getY() - yError.getX()), + dataYtoPixel(yAxis, next.getY() + yError.getY())); + } + ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); + */ } - ds.getSymbol().drawError(g, pixelX, pixelY, xError, yError); - */ - } - /** - * - * @param axis radial axis - * @param r radius - * @param theta in radians - * @return (x,y) coordinates of data - */ - public Point2D dataToPixel(AxisR axis, double r, double theta) { - return new Point2D.Double( - origin.getX() + radius * r / axis.getRange().getEnd() * Math.cos(theta), - origin.getY() - radius * r / axis.getRange().getEnd() * Math.sin(theta)); - } + /** + * + * @param axis radial axis + * @param r radius + * @param theta in radians + * @return (x,y) coordinates of data + */ + public Point2D dataToPixel(AxisR axis, double r, double theta) { + return new Point2D.Double( + origin.getX() + radius * r / axis.getRange().getEnd() * Math.cos(theta), + origin.getY() - radius * r / axis.getRange().getEnd() * Math.sin(theta)); + } - /** - * @param axis radial axis - * @param pixel pixel location - * @return r, theta (in radians) of (x,y) coordinates - */ - public Point2D pixelToData(AxisR axis, Point2D pixel) { - double x = pixel.getX() - origin.getX(); - double y = pixel.getY() - origin.getY(); + /** + * @param axis radial axis + * @param pixel pixel location + * @return r, theta (in radians) of (x,y) coordinates + */ + public Point2D pixelToData(AxisR axis, Point2D pixel) { + double x = pixel.getX() - origin.getX(); + double y = pixel.getY() - origin.getY(); - double r = Math.sqrt(x * x + y * y); - double theta = Math.atan2(y, x); - - return new Point2D.Double(r, theta); - } + double r = Math.sqrt(x * x + y * y); + double theta = Math.atan2(y, x); + return new Point2D.Double(r, theta); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/RectangularPlotCanvas.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/RectangularPlotCanvas.java index b27f22c..e8b0f20 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/RectangularPlotCanvas.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/RectangularPlotCanvas.java @@ -92,7 +92,11 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { configureHintsForSubpixelQuality(g); g.setFont(config.axisFont()); g.setColor(Color.BLACK); - g.drawLine(config.leftMargin(), pageHeight - config.bottomMargin(), pageWidth - config.rightMargin(), pageHeight - config.bottomMargin()); + g.drawLine( + config.leftMargin(), + pageHeight - config.bottomMargin(), + pageWidth - config.rightMargin(), + pageHeight - config.bottomMargin()); if (xLowerAxis == null) return; g.setColor(xLowerAxis.getAxisColor()); @@ -104,7 +108,8 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { if (!xLowerAxis.getRange().closedContains(minorTick)) continue; Path2D.Double path = new Path2D.Double(); - double pixelX = xLowerAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), minorTick); + double pixelX = + xLowerAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), minorTick); double pixelY = pageHeight - config.bottomMargin(); path.moveTo(pixelX, pixelY); pixelY -= config.xMinorTickLength(); @@ -132,7 +137,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { pixelY = config.getBottomPlotEdge(); Rectangle2D bb = StringUtils.boundingBox(g, tickLabels.get(majorTick)); - double height = Math.max(bb.getHeight() * Math.abs(Math.cos(xLowerAxis.getRotateLabels())), bb.getWidth() * Math.abs(Math.sin(xLowerAxis.getRotateLabels()))); + double height = Math.max( + bb.getHeight() * Math.abs(Math.cos(xLowerAxis.getRotateLabels())), + bb.getWidth() * Math.abs(Math.sin(xLowerAxis.getRotateLabels()))); pixelY += (g.getFontMetrics().getHeight() + height / 2); labelEdge = Math.max(labelEdge, pixelY); @@ -147,7 +154,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { // we want the bounding box centered at pixelX, pixelY Rectangle2D bb = StringUtils.boundingBox(g, xLowerAxis.getTitle()); - double height = Math.max(bb.getHeight() * Math.abs(Math.cos(xLowerAxis.getRotateTitle())), bb.getWidth() * Math.abs(Math.sin(xLowerAxis.getRotateTitle()))); + double height = Math.max( + bb.getHeight() * Math.abs(Math.cos(xLowerAxis.getRotateTitle())), + bb.getWidth() * Math.abs(Math.sin(xLowerAxis.getRotateTitle()))); pixelY += height; addAnnotation(g, xLowerAxis.getTitle(), pixelX, pixelY, xLowerAxis.getRotateTitle()); @@ -171,7 +180,8 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { if (!xUpperAxis.getRange().closedContains(minorTick)) continue; Path2D.Double path = new Path2D.Double(); - double pixelX = xUpperAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), minorTick); + double pixelX = + xUpperAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), minorTick); double pixelY = config.topMargin(); path.moveTo(pixelX, pixelY); pixelY += config.xMinorTickLength(); @@ -198,7 +208,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { pixelY = config.topMargin(); Rectangle2D bb = StringUtils.boundingBox(g, tickLabels.get(majorTick)); - double height = Math.max(bb.getHeight() * Math.abs(Math.cos(xUpperAxis.getRotateLabels())), bb.getWidth() * Math.abs(Math.sin(xUpperAxis.getRotateLabels()))); + double height = Math.max( + bb.getHeight() * Math.abs(Math.cos(xUpperAxis.getRotateLabels())), + bb.getWidth() * Math.abs(Math.sin(xUpperAxis.getRotateLabels()))); pixelY -= (g.getFontMetrics().getHeight() + height / 2); labelEdge = Math.min(labelEdge, pixelY); @@ -213,7 +225,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { // we want the bounding box centered at pixelX, pixelY Rectangle2D bb = StringUtils.boundingBox(g, xUpperAxis.getTitle()); - double height = Math.max(bb.getHeight() * Math.abs(Math.cos(xUpperAxis.getRotateTitle())), bb.getWidth() * Math.abs(Math.sin(xUpperAxis.getRotateTitle()))); + double height = Math.max( + bb.getHeight() * Math.abs(Math.cos(xUpperAxis.getRotateTitle())), + bb.getWidth() * Math.abs(Math.sin(xUpperAxis.getRotateTitle()))); pixelY -= height; addAnnotation(g, xUpperAxis.getTitle(), pixelX, pixelY, xUpperAxis.getRotateTitle()); @@ -238,7 +252,8 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { Path2D.Double path = new Path2D.Double(); double pixelX = config.leftMargin(); - double pixelY = yLeftAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), minorTick); + double pixelY = + yLeftAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), minorTick); path.moveTo(pixelX, pixelY); pixelX += config.yMinorTickLength(); path.lineTo(pixelX, pixelY); @@ -264,7 +279,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { pixelX = config.leftMargin(); Rectangle2D bb = StringUtils.boundingBox(g, tickLabels.get(majorTick)); - double width = Math.max(bb.getWidth() * Math.abs(Math.cos(yLeftAxis.getRotateLabels())), bb.getHeight() * Math.abs(Math.sin(yLeftAxis.getRotateLabels()))); + double width = Math.max( + bb.getWidth() * Math.abs(Math.cos(yLeftAxis.getRotateLabels())), + bb.getHeight() * Math.abs(Math.sin(yLeftAxis.getRotateLabels()))); pixelX -= (g.getFontMetrics().getMaxAdvance() + width / 2); labelEdge = Math.min(labelEdge, pixelX); @@ -279,7 +296,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { // we want the bounding box centered at pixelX, pixelY Rectangle2D bb = StringUtils.boundingBox(g, yLeftAxis.getTitle()); - double width = Math.max(bb.getWidth() * Math.abs(Math.cos(yLeftAxis.getRotateTitle())), bb.getHeight() * Math.abs(Math.sin(yLeftAxis.getRotateTitle()))); + double width = Math.max( + bb.getWidth() * Math.abs(Math.cos(yLeftAxis.getRotateTitle())), + bb.getHeight() * Math.abs(Math.sin(yLeftAxis.getRotateTitle()))); pixelX -= width; addAnnotation(g, yLeftAxis.getTitle(), pixelX, pixelY, yLeftAxis.getRotateTitle()); @@ -291,7 +310,11 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { configureHintsForSubpixelQuality(g); g.setFont(config.axisFont()); g.setColor(Color.BLACK); - g.drawLine(pageWidth - config.rightMargin(), config.topMargin(), pageWidth - config.rightMargin(), pageHeight - config.bottomMargin()); + g.drawLine( + pageWidth - config.rightMargin(), + config.topMargin(), + pageWidth - config.rightMargin(), + pageHeight - config.bottomMargin()); if (yRightAxis == null) return; g.setColor(yRightAxis.getAxisColor()); @@ -304,7 +327,8 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { Path2D.Double path = new Path2D.Double(); double pixelX = pageWidth - config.rightMargin(); - double pixelY = yRightAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), minorTick); + double pixelY = + yRightAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), minorTick); path.moveTo(pixelX, pixelY); pixelX -= config.yMinorTickLength(); path.lineTo(pixelX, pixelY); @@ -330,7 +354,9 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { pixelX = config.getRightPlotEdge(); Rectangle2D bb = StringUtils.boundingBox(g, tickLabels.get(majorTick)); - double width = Math.max(bb.getWidth() * Math.abs(Math.cos(yRightAxis.getRotateLabels())), bb.getHeight() * Math.abs(Math.sin(yRightAxis.getRotateLabels()))); + double width = Math.max( + bb.getWidth() * Math.abs(Math.cos(yRightAxis.getRotateLabels())), + bb.getHeight() * Math.abs(Math.sin(yRightAxis.getRotateLabels()))); pixelX += (g.getFontMetrics().getMaxAdvance() + width / 2); labelEdge = Math.max(labelEdge, pixelX); @@ -345,12 +371,13 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { // we want the bounding box centered at pixelX, pixelY Rectangle2D bb = StringUtils.boundingBox(g, yRightAxis.getTitle()); - double width = Math.max(bb.getWidth() * Math.abs(Math.cos(yRightAxis.getRotateTitle())), bb.getHeight() * Math.abs(Math.sin(yRightAxis.getRotateTitle()))); + double width = Math.max( + bb.getWidth() * Math.abs(Math.cos(yRightAxis.getRotateTitle())), + bb.getHeight() * Math.abs(Math.sin(yRightAxis.getRotateTitle()))); pixelX += width; addAnnotation(g, yRightAxis.getTitle(), pixelX, pixelY, yRightAxis.getRotateTitle()); } - } /** @@ -463,8 +490,14 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { for (double majorTick : tickLabels.keySet()) { if (!xAxis.getRange().closedContains(majorTick)) continue; double pixelX = xAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), majorTick); - double pixelY0 = yAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), yAxis.getRange().getBegin()); - double pixelY1 = yAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), yAxis.getRange().getEnd()); + double pixelY0 = yAxis.dataToPixel( + pageHeight - config.bottomMargin(), + config.topMargin(), + yAxis.getRange().getBegin()); + double pixelY1 = yAxis.dataToPixel( + pageHeight - config.bottomMargin(), + config.topMargin(), + yAxis.getRange().getEnd()); Path2D.Double path = new Path2D.Double(); path.moveTo(pixelX, pixelY0); @@ -477,8 +510,14 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { for (double majorTick : tickLabels.keySet()) { if (!yAxis.getRange().closedContains(majorTick)) continue; double pixelY = yAxis.dataToPixel(pageHeight - config.bottomMargin(), config.topMargin(), majorTick); - double pixelX0 = xAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), xAxis.getRange().getBegin()); - double pixelX1 = xAxis.dataToPixel(config.leftMargin(), pageWidth - config.rightMargin(), xAxis.getRange().getEnd()); + double pixelX0 = xAxis.dataToPixel( + config.leftMargin(), + pageWidth - config.rightMargin(), + xAxis.getRange().getBegin()); + double pixelX1 = xAxis.dataToPixel( + config.leftMargin(), + pageWidth - config.rightMargin(), + xAxis.getRange().getEnd()); Path2D.Double path = new Path2D.Double(); path.moveTo(pixelX0, pixelY); @@ -496,8 +535,7 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { * @param color color to use for shading */ public void shadeRange(AxisX axis, Collection range, Color color) { - for (Interval interval : range) - shadeRange(axis, interval, color); + for (Interval interval : range) shadeRange(axis, interval, color); } /** @@ -531,8 +569,7 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { * @param color color to use for shading */ public void shadeRange(AxisY axis, Collection range, Color color) { - for (Interval interval : range) - shadeRange(axis, interval, color); + for (Interval interval : range) shadeRange(axis, interval, color); } /** @@ -593,5 +630,4 @@ public abstract class RectangularPlotCanvas extends PlotCanvas { public double pixelYtoData(AxisY axis, double y) { return axis.pixelToData(pageHeight - config.bottomMargin(), config.topMargin(), y); } - } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/Axis.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/Axis.java index d4608b0..3b8c9ed 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/Axis.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/Axis.java @@ -33,178 +33,177 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public abstract class Axis { - private static final Logger logger = LogManager.getLogger(Axis.class); + private static final Logger logger = LogManager.getLogger(Axis.class); - protected TickMarks tickMarks; - protected Color axisColor; - protected AxisRange range; - protected boolean isLog; - protected AxisRange logRange; - protected NavigableMap tickLabels; - protected String title; - protected Function tickFunc; - protected double rotateTitle; - protected double rotateLabels; + protected TickMarks tickMarks; + protected Color axisColor; + protected AxisRange range; + protected boolean isLog; + protected AxisRange logRange; + protected NavigableMap tickLabels; + protected String title; + protected Function tickFunc; + protected double rotateTitle; + protected double rotateLabels; - protected Axis(Axis other) { - this(other.getRange().getBegin(), other.getRange().getEnd(), other.getTitle()); - setTo(other); - } - - protected Axis(double begin, double end, String title) { - this(begin, end, title, StringFunctions.fixedFormat("%.2f")); - } - - protected Axis(double begin, double end, String title, Function tickFunc) { - axisColor = Color.black; - range = new AxisRange(begin, end); - rotateTitle = 0; - rotateLabels = 0; - isLog = false; - logRange = null; - - if (range.getLength() < 2e-15) { - range = new AxisRange(range.getMiddle() - 1e-15, range.getMiddle() + 1e-15); - logger.warn( - String.format( - "%s: Axis length is %g! Setting to [%e, %e].", - title, end - begin, range.getBegin(), range.getEnd())); + protected Axis(Axis other) { + this(other.getRange().getBegin(), other.getRange().getEnd(), other.getTitle()); + setTo(other); } - this.tickMarks = new TickMarks(range, isLog); - - this.title = title; - if (title == null) this.title = ""; - - setTickLabelFunction(tickFunc); - } - - protected void setTo(Axis other) { - this.tickMarks = new TickMarks(other.tickMarks); - this.axisColor = other.axisColor; - this.range = other.range; - this.isLog = other.isLog; - this.logRange = other.logRange; - this.tickLabels = new TreeMap<>(other.tickLabels); - this.title = other.title; - this.tickFunc = other.tickFunc; - this.rotateTitle = other.rotateTitle; - this.rotateLabels = other.rotateLabels; - } - - public boolean isLog() { - return isLog; - } - - public void setLog(boolean isLog) { - this.isLog = isLog; - if (isLog) { - if (range.getBegin() <= 0 || range.getEnd() <= 0) { - throw new RuntimeException(String.format("Cannot create log axis with range %s\n", range)); - } - - tickMarks = new TickMarks(range, true); - logRange = new AxisRange(Math.log10(range.getBegin()), Math.log10(range.getEnd())); - - setTickLabelFunction(tickFunc); + protected Axis(double begin, double end, String title) { + this(begin, end, title, StringFunctions.fixedFormat("%.2f")); } - } - public void setNMinorTicks(int nMinorTicks) { - tickMarks.setNMinorTicks(nMinorTicks); - } + protected Axis(double begin, double end, String title, Function tickFunc) { + axisColor = Color.black; + range = new AxisRange(begin, end); + rotateTitle = 0; + rotateLabels = 0; + isLog = false; + logRange = null; - public void setTickLabels(NavigableMap labels, int nMinorTicks) { - tickLabels = new TreeMap<>(); - for (Double tick : labels.keySet()) { - tickLabels.put(tick, labels.get(tick)); + if (range.getLength() < 2e-15) { + range = new AxisRange(range.getMiddle() - 1e-15, range.getMiddle() + 1e-15); + logger.warn(String.format( + "%s: Axis length is %g! Setting to [%e, %e].", + title, end - begin, range.getBegin(), range.getEnd())); + } + + this.tickMarks = new TickMarks(range, isLog); + + this.title = title; + if (title == null) this.title = ""; + + setTickLabelFunction(tickFunc); } - tickMarks.setMajorTicks(tickLabels.navigableKeySet()); - setNMinorTicks(nMinorTicks); - } - public Function getTickLabelFunction() { - return tickFunc; - } - - public void setTickLabelFunction(Function tickFunc) { - this.tickFunc = tickFunc; - tickLabels = new TreeMap<>(); - for (double tick : tickMarks.getMajorTicks()) { - tickLabels.put(tick, tickFunc.apply(tick)); + protected void setTo(Axis other) { + this.tickMarks = new TickMarks(other.tickMarks); + this.axisColor = other.axisColor; + this.range = other.range; + this.isLog = other.isLog; + this.logRange = other.logRange; + this.tickLabels = new TreeMap<>(other.tickLabels); + this.title = other.title; + this.tickFunc = other.tickFunc; + this.rotateTitle = other.rotateTitle; + this.rotateLabels = other.rotateLabels; } - } - public double dataToPixel(int pixelMin, int pixelMax, double value) { - double frac = (value - range.getBegin()) / (range.getEnd() - range.getBegin()); - if (isLog) { - frac = (Math.log10(value) - logRange.getBegin()) / (logRange.getEnd() - logRange.getBegin()); + public boolean isLog() { + return isLog; } - return (pixelMin + frac * (pixelMax - pixelMin)); - } - public double pixelToData(int pixelMin, int pixelMax, double value) { - double frac = (value - pixelMin) / (pixelMax - pixelMin); - if (isLog) return (logRange.getBegin() + frac * (logRange.getEnd() - logRange.getBegin())); - else return (range.getBegin() + frac * (range.getEnd() - range.getBegin())); - } + public void setLog(boolean isLog) { + this.isLog = isLog; + if (isLog) { + if (range.getBegin() <= 0 || range.getEnd() <= 0) { + throw new RuntimeException(String.format("Cannot create log axis with range %s\n", range)); + } - public Color getAxisColor() { - return axisColor; - } + tickMarks = new TickMarks(range, true); + logRange = new AxisRange(Math.log10(range.getBegin()), Math.log10(range.getEnd())); - public void setAxisColor(Color axisColor) { - this.axisColor = axisColor; - } + setTickLabelFunction(tickFunc); + } + } - public String getTitle() { - return title; - } + public void setNMinorTicks(int nMinorTicks) { + tickMarks.setNMinorTicks(nMinorTicks); + } - public void setTitle(String title) { - this.title = title; - } + public void setTickLabels(NavigableMap labels, int nMinorTicks) { + tickLabels = new TreeMap<>(); + for (Double tick : labels.keySet()) { + tickLabels.put(tick, labels.get(tick)); + } + tickMarks.setMajorTicks(tickLabels.navigableKeySet()); + setNMinorTicks(nMinorTicks); + } - public AxisRange getRange() { - return range; - } + public Function getTickLabelFunction() { + return tickFunc; + } - public NavigableSet getMajorTicks() { - return tickMarks.getMajorTicks(); - } + public void setTickLabelFunction(Function tickFunc) { + this.tickFunc = tickFunc; + tickLabels = new TreeMap<>(); + for (double tick : tickMarks.getMajorTicks()) { + tickLabels.put(tick, tickFunc.apply(tick)); + } + } - public NavigableMap getTickLabels() { - return tickLabels; - } + public double dataToPixel(int pixelMin, int pixelMax, double value) { + double frac = (value - range.getBegin()) / (range.getEnd() - range.getBegin()); + if (isLog) { + frac = (Math.log10(value) - logRange.getBegin()) / (logRange.getEnd() - logRange.getBegin()); + } + return (pixelMin + frac * (pixelMax - pixelMin)); + } - public void setTickLabels(NavigableMap labels) { - setTickLabels(labels, 0); - } + public double pixelToData(int pixelMin, int pixelMax, double value) { + double frac = (value - pixelMin) / (pixelMax - pixelMin); + if (isLog) return (logRange.getBegin() + frac * (logRange.getEnd() - logRange.getBegin())); + else return (range.getBegin() + frac * (range.getEnd() - range.getBegin())); + } - public NavigableSet getMinorTicks() { - return tickMarks.getMinorTicks(); - } + public Color getAxisColor() { + return axisColor; + } - public void setMinorTicks(NavigableSet minorTicks) { - tickMarks.setMinorTicks(minorTicks); - } + public void setAxisColor(Color axisColor) { + this.axisColor = axisColor; + } - public double getRotateTitle() { - return rotateTitle; - } + public String getTitle() { + return title; + } - public void setRotateTitle(double rotateTitle) { - this.rotateTitle = rotateTitle; - } + public void setTitle(String title) { + this.title = title; + } - public double getRotateLabels() { - return rotateLabels; - } + public AxisRange getRange() { + return range; + } - /** - * @param rotateLabels Angle to rotate label clockwise, in radians - */ - public void setRotateLabels(double rotateLabels) { - this.rotateLabels = rotateLabels; - } + public NavigableSet getMajorTicks() { + return tickMarks.getMajorTicks(); + } + + public NavigableMap getTickLabels() { + return tickLabels; + } + + public void setTickLabels(NavigableMap labels) { + setTickLabels(labels, 0); + } + + public NavigableSet getMinorTicks() { + return tickMarks.getMinorTicks(); + } + + public void setMinorTicks(NavigableSet minorTicks) { + tickMarks.setMinorTicks(minorTicks); + } + + public double getRotateTitle() { + return rotateTitle; + } + + public void setRotateTitle(double rotateTitle) { + this.rotateTitle = rotateTitle; + } + + public double getRotateLabels() { + return rotateLabels; + } + + /** + * @param rotateLabels Angle to rotate label clockwise, in radians + */ + public void setRotateLabels(double rotateLabels) { + this.rotateLabels = rotateLabels; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisR.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisR.java index 119e6b4..18b9566 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisR.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisR.java @@ -27,16 +27,15 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class AxisR extends Axis { - public AxisR(double min, double max, String title, Function func) { - super(min, max, title, func); - } + public AxisR(double min, double max, String title, Function func) { + super(min, max, title, func); + } - public AxisR(double min, double max, String title, String tickFormat) { - super(min, max, title, StringFunctions.fixedFormat(tickFormat)); - } - - public AxisR(double min, double max, String title) { - super(min, max, title); - } + public AxisR(double min, double max, String title, String tickFormat) { + super(min, max, title, StringFunctions.fixedFormat(tickFormat)); + } + public AxisR(double min, double max, String title) { + super(min, max, title); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRange.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRange.java index f5e42e9..df24cda 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRange.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRange.java @@ -29,250 +29,246 @@ import org.apache.logging.log4j.Logger; /** * Range to use for an Axis. Similar to {@link Interval} but the beginning value can be * greater than the end for decreasing axes. - * + * * @author Hari.Nair@jhuapl.edu * */ public class AxisRange { - private final static Logger logger = LogManager.getLogger(AxisRange.class); + private static final Logger logger = LogManager.getLogger(AxisRange.class); - private final double begin; - private final double end; + private final double begin; + private final double end; - private final double min; - private final double max; - private final double length; + private final double min; + private final double max; + private final double length; - private final boolean isDecreasing; + private final boolean isDecreasing; - /** - * - * @return starting (left) value - */ - public double getBegin() { - return begin; - } - - /** - * - * @return ending (right) value - */ - public double getEnd() { - return end; - } - - /** - * - * @return the smaller endpoint - */ - public double getMin() { - return min; - } - - /** - * - * @return the larger endpoint - */ - public double getMax() { - return max; - } - - /** - * - * @return the middle point in the interval: (this.begin + this.end)/2.0 - */ - public double getMiddle() { - return (end + begin) / 2.0; - } - - /** - * - * @return the value of {@link #getMax()} - {@link #getMin()}. This is always a non-negative - * value. - */ - public double getLength() { - return length; - } - - /** - * - * @return true if {@link #getBegin()} > {@link #getEnd()} - */ - public boolean isDecreasing() { - return isDecreasing; - } - - /** - * Define a range where the beginning value may be greater than the ending value. - * - * @param begin leftmost value - * @param end rightmost value - */ - public AxisRange(double begin, double end) { - this.begin = begin; - this.end = end; - this.min = Math.min(begin, end); - this.max = Math.max(begin, end); - this.length = this.max - this.min; - this.isDecreasing = begin > end; - } - - /** - * Is the supplied value contained within the interval in a [,] fashion? - * - * @param value the value to consider for containment - * - * @return true if value lies in [this.min,this.max]; false otherwise - */ - public boolean closedContains(double value) { - return (value >= min) && (value <= max); - } - - /** - * Clamps the supplied value into the range supported by the interval. This function returns: - *

      - *
    • ( value ≤ begin ) -> begin
    • - *
    • ( value ≥ end ) -> end
    • - *
    • value, otherwise
    • - *
    - * - * @param value the value to clamp - * - * @return a value in the range [this.begin, this.end] - */ - public double clamp(double value) { - return Math.min(Math.max(min, value), max); - } - - /** - *

    - * Examples: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    minAxis beginmaxAxis end
    1.3314.5510
    1.33145.5100
    0.01330.014551000
    - * - * @param min minimum value. Axis minimum will be the highest power of ten equal to or less than min. - * @param max maximum value. Axis maximum will be the lowest power of ten equal to or greater than max. - * @return axis range with the lower and upper limits rounded to the next lower and higher - * powers of ten. - */ - public static AxisRange getLogAxis(double min, double max) { - if (min <= 0) { - logger.error( - new RuntimeException("Minimum " + min + " passed to getLogAxis()! Returning null.")); - return null; + /** + * + * @return starting (left) value + */ + public double getBegin() { + return begin; } - if (max <= 0) { - logger.error( - new RuntimeException("Maximum " + max + " passed to getLogAxis()! Returning null.")); - return null; + /** + * + * @return ending (right) value + */ + public double getEnd() { + return end; } - int minExp = (int) Math.floor(Math.log10(min)); - int maxExp = (int) Math.ceil(Math.log10(max)); - double begin = Math.pow(10., minExp); - double end = Math.pow(10., maxExp); - return new AxisRange(begin, end); - } - - /** - *

    - * Examples for a min of 0.01234567 and max of 12.34567: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    PrecisionAxis beginAxis end
    10.0120
    20.01213
    30.012312.4
    40.0123412.35
    50.01234512.346
    60.012345612.3457
    - * - * @param min minimum value. Axis minimum will be equal to this value using the desired number of significant figures - * @param max maximum value. Axis maximum will be equal to this value using the desired number of significant figures - * @param precision number of significant figures to use - * @return axis range with the lower and upper limits having the desired number of significant - * figures. - */ - public static AxisRange getLinearAxis(double min, double max, int precision) { - - if (precision <= 0) { - logger.error(new RuntimeException( - "Precision " + precision + " passed to getLinearAxis()! Returning null.")); - return null; + /** + * + * @return the smaller endpoint + */ + public double getMin() { + return min; } - double scale = Math.pow(10, precision - 1); - - double roundedMax = 0; - if (max != 0) { - int exp = (int) Math.floor(Math.log10(Math.abs(max))); - double mantissa = max / Math.pow(10, exp); - roundedMax = Math.ceil(mantissa * scale) / scale * Math.pow(10, exp); + /** + * + * @return the larger endpoint + */ + public double getMax() { + return max; } - double roundedMin = 0; - if (min != 0) { - int exp = (int) Math.floor(Math.log10(Math.abs(min))); - double mantissa = min / Math.pow(10, exp); - roundedMin = Math.floor(mantissa * scale) / scale * Math.pow(10, exp); - + /** + * + * @return the middle point in the interval: (this.begin + this.end)/2.0 + */ + public double getMiddle() { + return (end + begin) / 2.0; } - return new AxisRange(roundedMin, roundedMax); - } + /** + * + * @return the value of {@link #getMax()} - {@link #getMin()}. This is always a non-negative + * value. + */ + public double getLength() { + return length; + } + /** + * + * @return true if {@link #getBegin()} > {@link #getEnd()} + */ + public boolean isDecreasing() { + return isDecreasing; + } + + /** + * Define a range where the beginning value may be greater than the ending value. + * + * @param begin leftmost value + * @param end rightmost value + */ + public AxisRange(double begin, double end) { + this.begin = begin; + this.end = end; + this.min = Math.min(begin, end); + this.max = Math.max(begin, end); + this.length = this.max - this.min; + this.isDecreasing = begin > end; + } + + /** + * Is the supplied value contained within the interval in a [,] fashion? + * + * @param value the value to consider for containment + * + * @return true if value lies in [this.min,this.max]; false otherwise + */ + public boolean closedContains(double value) { + return (value >= min) && (value <= max); + } + + /** + * Clamps the supplied value into the range supported by the interval. This function returns: + *

      + *
    • ( value ≤ begin ) -> begin
    • + *
    • ( value ≥ end ) -> end
    • + *
    • value, otherwise
    • + *
    + * + * @param value the value to clamp + * + * @return a value in the range [this.begin, this.end] + */ + public double clamp(double value) { + return Math.min(Math.max(min, value), max); + } + + /** + *

    + * Examples: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    minAxis beginmaxAxis end
    1.3314.5510
    1.33145.5100
    0.01330.014551000
    + * + * @param min minimum value. Axis minimum will be the highest power of ten equal to or less than min. + * @param max maximum value. Axis maximum will be the lowest power of ten equal to or greater than max. + * @return axis range with the lower and upper limits rounded to the next lower and higher + * powers of ten. + */ + public static AxisRange getLogAxis(double min, double max) { + if (min <= 0) { + logger.error(new RuntimeException("Minimum " + min + " passed to getLogAxis()! Returning null.")); + return null; + } + + if (max <= 0) { + logger.error(new RuntimeException("Maximum " + max + " passed to getLogAxis()! Returning null.")); + return null; + } + + int minExp = (int) Math.floor(Math.log10(min)); + int maxExp = (int) Math.ceil(Math.log10(max)); + double begin = Math.pow(10., minExp); + double end = Math.pow(10., maxExp); + return new AxisRange(begin, end); + } + + /** + *

    + * Examples for a min of 0.01234567 and max of 12.34567: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    PrecisionAxis beginAxis end
    10.0120
    20.01213
    30.012312.4
    40.0123412.35
    50.01234512.346
    60.012345612.3457
    + * + * @param min minimum value. Axis minimum will be equal to this value using the desired number of significant figures + * @param max maximum value. Axis maximum will be equal to this value using the desired number of significant figures + * @param precision number of significant figures to use + * @return axis range with the lower and upper limits having the desired number of significant + * figures. + */ + public static AxisRange getLinearAxis(double min, double max, int precision) { + + if (precision <= 0) { + logger.error( + new RuntimeException("Precision " + precision + " passed to getLinearAxis()! Returning null.")); + return null; + } + + double scale = Math.pow(10, precision - 1); + + double roundedMax = 0; + if (max != 0) { + int exp = (int) Math.floor(Math.log10(Math.abs(max))); + double mantissa = max / Math.pow(10, exp); + roundedMax = Math.ceil(mantissa * scale) / scale * Math.pow(10, exp); + } + + double roundedMin = 0; + if (min != 0) { + int exp = (int) Math.floor(Math.log10(Math.abs(min))); + double mantissa = min / Math.pow(10, exp); + roundedMin = Math.floor(mantissa * scale) / scale * Math.pow(10, exp); + } + + return new AxisRange(roundedMin, roundedMax); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisTheta.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisTheta.java index 218e712..989581d 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisTheta.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisTheta.java @@ -27,16 +27,15 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class AxisTheta extends Axis { - public AxisTheta(double min, double max, String title, Function tickFunc) { - super(min, max, title, tickFunc); - } + public AxisTheta(double min, double max, String title, Function tickFunc) { + super(min, max, title, tickFunc); + } - public AxisTheta(double min, double max, String title, String tickFormat) { - super(min, max, title, StringFunctions.fixedFormat(tickFormat)); - } - - public AxisTheta(double min, double max, String title) { - super(min, max, title); - } + public AxisTheta(double min, double max, String title, String tickFormat) { + super(min, max, title, StringFunctions.fixedFormat(tickFormat)); + } + public AxisTheta(double min, double max, String title) { + super(min, max, title); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisX.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisX.java index eb10a7b..0e67fd4 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisX.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisX.java @@ -27,19 +27,19 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class AxisX extends Axis { - public AxisX(double min, double max, String title, Function func) { - super(min, max, title, func); - } + public AxisX(double min, double max, String title, Function func) { + super(min, max, title, func); + } - public AxisX(double min, double max, String title, String tickFormat) { - super(min, max, title, StringFunctions.fixedFormat(tickFormat)); - } + public AxisX(double min, double max, String title, String tickFormat) { + super(min, max, title, StringFunctions.fixedFormat(tickFormat)); + } - public AxisX(double min, double max, String title) { - super(min, max, title); - } + public AxisX(double min, double max, String title) { + super(min, max, title); + } - public AxisX(AxisX other) { - super(other); - } + public AxisX(AxisX other) { + super(other); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisY.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisY.java index 8244394..1a50127 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisY.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisY.java @@ -27,22 +27,21 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class AxisY extends Axis { - public AxisY(double min, double max, String title, Function func) { - super(min, max, title, func); - setRotateTitle(-Math.PI / 2); - } + public AxisY(double min, double max, String title, Function func) { + super(min, max, title, func); + setRotateTitle(-Math.PI / 2); + } - public AxisY(double min, double max, String title) { - super(min, max, title); - setRotateTitle(-Math.PI / 2); - } + public AxisY(double min, double max, String title) { + super(min, max, title); + setRotateTitle(-Math.PI / 2); + } - public AxisY(double min, double max, String title, String tickFormat) { - this(min, max, title, StringFunctions.fixedFormat(tickFormat)); - } - - public AxisY(AxisY other) { - super(other); - } + public AxisY(double min, double max, String title, String tickFormat) { + this(min, max, title, StringFunctions.fixedFormat(tickFormat)); + } + public AxisY(AxisY other) { + super(other); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarks.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarks.java index f46b6c0..231dd84 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarks.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarks.java @@ -29,126 +29,126 @@ import java.util.TreeSet; // package private, called from Axis class TickMarks { - private double tickSpacing; + private double tickSpacing; - private final boolean isLog; + private final boolean isLog; - protected final AxisRange range; + protected final AxisRange range; - public AxisRange getRange() { - return range; - } - - private NavigableSet majorTicks; - - public NavigableSet getMajorTicks() { - return majorTicks; - } - - private NavigableSet minorTicks; - - public NavigableSet getMinorTicks() { - return minorTicks; - } - - TickMarks(TickMarks other) { - this.tickSpacing = other.tickSpacing; - this.isLog = other.isLog; - this.range = new AxisRange(other.getRange().getBegin(), other.getRange().getEnd()); - majorTicks = Collections.unmodifiableNavigableSet(other.majorTicks); - minorTicks = Collections.unmodifiableNavigableSet(other.minorTicks); - } - - TickMarks(AxisRange range, boolean isLog) { - - this.range = range; - this.isLog = isLog; - - if (isLog) { - if (range.getBegin() <= 0 || range.getEnd() <= 0 || range.getLength() == 0) { - throw new RuntimeException(String.format("Cannot create log axis with range %s\n", range)); - } - - range = new AxisRange(Math.log10(range.getBegin()), Math.log10(range.getEnd())); + public AxisRange getRange() { + return range; } - tickSpacing = Math.pow(10, Math.floor(Math.log10(range.getLength()))); - int nTicks = (int) Math.ceil(range.getLength() / tickSpacing); + private NavigableSet majorTicks; - while (nTicks < 5) { - tickSpacing /= 2; - nTicks = (int) Math.ceil(range.getLength() / tickSpacing); - - if (nTicks == 0) { - nTicks = 5; - tickSpacing = range.getLength() / nTicks; - break; - } + public NavigableSet getMajorTicks() { + return majorTicks; } - double begin = tickSpacing * Math.ceil(range.getMin() / tickSpacing); - double end = nTicks * tickSpacing + begin; - setNTicks(nTicks, new AxisRange(begin, end)); - setNMinorTicks(isLog ? 9 : 4); - } + private NavigableSet minorTicks; - protected void setNTicks(int nTicks, AxisRange range) { - tickSpacing = range.getLength() / nTicks; - NavigableSet majorTicks = new TreeSet<>(); - if (isLog) { - double tick = Math.pow(10, range.getMin() - tickSpacing); - majorTicks.add(tick); - while (tick < Math.pow(10, range.getMax() + tickSpacing)) { - tick *= Math.pow(10, tickSpacing); - majorTicks.add(tick); - } - } else { - double tick = range.getMin() - tickSpacing; - while (tick < range.getMax() + tickSpacing) { - majorTicks.add(tick); - tick += tickSpacing; - } + public NavigableSet getMinorTicks() { + return minorTicks; } - setMajorTicks(majorTicks); - } - /** - * Divide the axis range into nTicks intervals. There will be nTicks-1 ticks between the axis - * begin and end. - * - * @param nTicks number of intervals - */ - public void setNTicks(int nTicks) { - setNTicks(nTicks, range); - } + TickMarks(TickMarks other) { + this.tickSpacing = other.tickSpacing; + this.isLog = other.isLog; + this.range = new AxisRange(other.getRange().getBegin(), other.getRange().getEnd()); + majorTicks = Collections.unmodifiableNavigableSet(other.majorTicks); + minorTicks = Collections.unmodifiableNavigableSet(other.minorTicks); + } - /** - * Divide each major tick interval into nMinorTicks. There will be nMinorTicks-1 minor ticks drawn - * in each interval. - * - * @param nMinorTicks number of intervals - */ - public void setNMinorTicks(int nMinorTicks) { - NavigableSet minorTicks = new TreeSet<>(); - for (Double majorTick : majorTicks) { - if (nMinorTicks > 0) { - Double nextTick = majorTicks.higher(majorTick); - if (nextTick != null) { - for (int i = 1; i < nMinorTicks; i++) { - double minorTick = i * (nextTick - majorTick) / nMinorTicks + majorTick; - minorTicks.add(minorTick); - } + TickMarks(AxisRange range, boolean isLog) { + + this.range = range; + this.isLog = isLog; + + if (isLog) { + if (range.getBegin() <= 0 || range.getEnd() <= 0 || range.getLength() == 0) { + throw new RuntimeException(String.format("Cannot create log axis with range %s\n", range)); + } + + range = new AxisRange(Math.log10(range.getBegin()), Math.log10(range.getEnd())); } - } + + tickSpacing = Math.pow(10, Math.floor(Math.log10(range.getLength()))); + int nTicks = (int) Math.ceil(range.getLength() / tickSpacing); + + while (nTicks < 5) { + tickSpacing /= 2; + nTicks = (int) Math.ceil(range.getLength() / tickSpacing); + + if (nTicks == 0) { + nTicks = 5; + tickSpacing = range.getLength() / nTicks; + break; + } + } + + double begin = tickSpacing * Math.ceil(range.getMin() / tickSpacing); + double end = nTicks * tickSpacing + begin; + setNTicks(nTicks, new AxisRange(begin, end)); + setNMinorTicks(isLog ? 9 : 4); } - setMinorTicks(minorTicks); - } - public void setMajorTicks(NavigableSet majorTicks) { - if (majorTicks != null) this.majorTicks = Collections.unmodifiableNavigableSet(majorTicks); - } + protected void setNTicks(int nTicks, AxisRange range) { + tickSpacing = range.getLength() / nTicks; + NavigableSet majorTicks = new TreeSet<>(); + if (isLog) { + double tick = Math.pow(10, range.getMin() - tickSpacing); + majorTicks.add(tick); + while (tick < Math.pow(10, range.getMax() + tickSpacing)) { + tick *= Math.pow(10, tickSpacing); + majorTicks.add(tick); + } + } else { + double tick = range.getMin() - tickSpacing; + while (tick < range.getMax() + tickSpacing) { + majorTicks.add(tick); + tick += tickSpacing; + } + } + setMajorTicks(majorTicks); + } - public void setMinorTicks(NavigableSet minorTicks) { - if (minorTicks != null) this.minorTicks = Collections.unmodifiableNavigableSet(minorTicks); - } + /** + * Divide the axis range into nTicks intervals. There will be nTicks-1 ticks between the axis + * begin and end. + * + * @param nTicks number of intervals + */ + public void setNTicks(int nTicks) { + setNTicks(nTicks, range); + } + + /** + * Divide each major tick interval into nMinorTicks. There will be nMinorTicks-1 minor ticks drawn + * in each interval. + * + * @param nMinorTicks number of intervals + */ + public void setNMinorTicks(int nMinorTicks) { + NavigableSet minorTicks = new TreeSet<>(); + for (Double majorTick : majorTicks) { + if (nMinorTicks > 0) { + Double nextTick = majorTicks.higher(majorTick); + if (nextTick != null) { + for (int i = 1; i < nMinorTicks; i++) { + double minorTick = i * (nextTick - majorTick) / nMinorTicks + majorTick; + minorTicks.add(minorTick); + } + } + } + } + setMinorTicks(minorTicks); + } + + public void setMajorTicks(NavigableSet majorTicks) { + if (majorTicks != null) this.majorTicks = Collections.unmodifiableNavigableSet(majorTicks); + } + + public void setMinorTicks(NavigableSet minorTicks) { + if (minorTicks != null) this.minorTicks = Collections.unmodifiableNavigableSet(minorTicks); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisX.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisX.java index 63e8748..7e37e6b 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisX.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisX.java @@ -32,157 +32,174 @@ import picante.time.UTCEpoch; public class UTCAxisX extends AxisX { - private enum TIMESCALES { - YEAR(365.25 * 86400), DAY(86400.), HOUR(3600.), MINUTE(60.), SECOND(1.), MILLISECOND(0.001); + private enum TIMESCALES { + YEAR(365.25 * 86400), + DAY(86400.), + HOUR(3600.), + MINUTE(60.), + SECOND(1.), + MILLISECOND(0.001); - public final double duration; + public final double duration; - TIMESCALES(double duration) { - this.duration = duration; + TIMESCALES(double duration) { + this.duration = duration; + } } - } + /** + * Write out tick marks in the format YYYY-DDDTHH:MM:SS.SSS + */ + public static final Function ISOD = + t -> TimeAdapter.getInstance().convertToUTC(t).toString(); - /** - * Write out tick marks in the format YYYY-DDDTHH:MM:SS.SSS - */ - public final static Function ISOD = t -> TimeAdapter.getInstance().convertToUTC(t).toString(); + /** + * Write out tick marks in the format YYYY-MM-DDTHH:MM:SS.SSS + */ + public static final Function ISOC = t -> { + UTCEpoch utc = TimeAdapter.getInstance().convertToUTC(t).createValueRoundedToMillisecs(); + int sec = (int) utc.getSec(); + int millis = (int) (1000 * (utc.getSec() - sec) + 0.5); + return String.format( + "%4d-%02d-%02dT%02d:%02d:%02d.%03d", + utc.getYear(), utc.getMonth(), utc.getDom(), utc.getHour(), utc.getMin(), sec, millis); + }; - /** - * Write out tick marks in the format YYYY-MM-DDTHH:MM:SS.SSS - */ - public final static Function ISOC = t -> { - UTCEpoch utc = TimeAdapter.getInstance().convertToUTC(t).createValueRoundedToMillisecs(); - int sec = (int) utc.getSec(); - int millis = (int) (1000 * (utc.getSec() - sec) + 0.5); - return String.format("%4d-%02d-%02dT%02d:%02d:%02d.%03d", utc.getYear(), utc.getMonth(), - utc.getDom(), utc.getHour(), utc.getMin(), sec, millis); - }; + /** + * + * @param min seconds past J2000 + * @param max seconds past J2000 + * @param title axis title + * @param nTicks just a hint, may not be the actual number of ticks in the end. Use + * {@link UTCAxisX#setTickLabels(java.util.NavigableMap)} to explicitly set the ticks. But + * if you're doing that, why are you using this class? + */ + public UTCAxisX(double min, double max, String title, int nTicks) { + super(min, max, title); - /** - * - * @param min seconds past J2000 - * @param max seconds past J2000 - * @param title axis title - * @param nTicks just a hint, may not be the actual number of ticks in the end. Use - * {@link UTCAxisX#setTickLabels(java.util.NavigableMap)} to explicitly set the ticks. But - * if you're doing that, why are you using this class? - */ - public UTCAxisX(double min, double max, String title, int nTicks) { - super(min, max, title); + TimeAdapter ta = TimeAdapter.getInstance(); + TimeSystem utcSystem = ta.getUTCTimeSys(); - TimeAdapter ta = TimeAdapter.getInstance(); - TimeSystem utcSystem = ta.getUTCTimeSys(); + TIMESCALES timeScale = TIMESCALES.YEAR; + for (TIMESCALES t : TIMESCALES.values()) { + if ((max - min) / t.duration > nTicks) { + timeScale = t; + break; + } + } + + UTCEpoch beginUTC = TimeAdapter.getInstance().convertToUTC(min).createValueRoundedToMillisecs(); + UTCEpoch endUTC = TimeAdapter.getInstance().convertToUTC(max).createValueRoundedToMillisecs(); + switch (timeScale) { + case YEAR: + beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), 0, 0, 0); + endUTC = utcSystem.add(new UTCEpoch(endUTC.getYear(), 0, 0, 0, 0), timeScale.duration); + endUTC = new UTCEpoch(endUTC.getYear(), 0, 0, 0, 0); + break; + case DAY: + beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), 0, 0, 0); + endUTC = utcSystem.add(new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), 0, 0, 0), timeScale.duration); + endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), 0, 0, 0); + break; + case HOUR: + beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), beginUTC.getHour(), 0, 0); + endUTC = utcSystem.add( + new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), 0, 0), timeScale.duration); + endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), 0, 0); + break; + case MINUTE: + beginUTC = + new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), beginUTC.getHour(), beginUTC.getMin(), 0); + endUTC = utcSystem.add( + new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), endUTC.getMin(), 0), + timeScale.duration); + endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), endUTC.getMin(), 0); + break; + case SECOND: + beginUTC = new UTCEpoch( + beginUTC.getYear(), + beginUTC.getDoy(), + beginUTC.getHour(), + beginUTC.getMin(), + Math.floor(beginUTC.getSec())); + endUTC = utcSystem.add( + new UTCEpoch( + endUTC.getYear(), + endUTC.getDoy(), + endUTC.getHour(), + endUTC.getMin(), + Math.floor(endUTC.getSec())), + timeScale.duration); + endUTC = new UTCEpoch( + endUTC.getYear(), + endUTC.getDoy(), + endUTC.getHour(), + endUTC.getMin(), + Math.floor(endUTC.getSec())); + break; + case MILLISECOND: + beginUTC = beginUTC.createValueRoundedToMillisecs(); + endUTC = endUTC.createValueRoundedToMillisecs(); + default: + break; + } + + // put axis in units of the appropriate timescale + JulianDate roundedBegin = JulianDate.fromUTCEpoch(beginUTC); + JulianDate roundedEnd = JulianDate.fromUTCEpoch(endUTC); + min = 0; + max = 86400 * (roundedEnd.getDate() - roundedBegin.getDate()) / timeScale.duration; + + AxisRange range = new AxisRange(min, max); + TickMarks tickMarks = new TickMarks(range, false); + + NavigableSet majorTicks = new TreeSet<>(); + for (Double tick : tickMarks.getMajorTicks()) { + JulianDate jd = new JulianDate(roundedBegin.getDate() + tick * timeScale.duration / 86400); + majorTicks.add(ta.convertToET(jd.toUTCEpoch())); + } + + NavigableSet minorTicks = new TreeSet<>(); + for (Double tick : tickMarks.getMinorTicks()) { + JulianDate jd = new JulianDate(roundedBegin.getDate() + tick * timeScale.duration / 86400); + minorTicks.add(ta.convertToET(jd.toUTCEpoch())); + } + + this.tickMarks = new TickMarks( + new AxisRange(ta.convertToET(roundedBegin.toUTCEpoch()), ta.convertToET(roundedEnd.toUTCEpoch())), + false); + tickMarks.setMajorTicks(majorTicks); + tickMarks.setMinorTicks(minorTicks); + this.tickMarks = tickMarks; + + setTickLabelFunction(ISOC); + + /*- + // put axis in units of the appropriate timescale + double roundedBegin = ta.convertToET(beginUTC); + double roundedEnd = ta.convertToET(endUTC); + min = 0; + max = (roundedEnd - roundedBegin) / timeScale.duration; + + AxisRange range = new AxisRange(min, max); + TickMarks tickMarks = new TickMarks(range, false); + + NavigableSet majorTicks = new TreeSet<>(); + for (Double tick : tickMarks.getMajorTicks()) { + majorTicks.add(roundedBegin + tick * timeScale.duration); + } + + NavigableSet minorTicks = new TreeSet<>(); + for (Double tick : tickMarks.getMinorTicks()) { + minorTicks.add(roundedBegin + tick * timeScale.duration); + } + + this.tickMarks = new TickMarks(new AxisRange(roundedBegin, roundedEnd), false); + tickMarks.setMajorTicks(majorTicks); + tickMarks.setMinorTicks(minorTicks); + + this.tickMarks = tickMarks; + */ - TIMESCALES timeScale = TIMESCALES.YEAR; - for (TIMESCALES t : TIMESCALES.values()) { - if ((max - min) / t.duration > nTicks) { - timeScale = t; - break; - } } - - UTCEpoch beginUTC = TimeAdapter.getInstance().convertToUTC(min).createValueRoundedToMillisecs(); - UTCEpoch endUTC = TimeAdapter.getInstance().convertToUTC(max).createValueRoundedToMillisecs(); - switch (timeScale) { - case YEAR: - beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), 0, 0, 0); - endUTC = utcSystem.add(new UTCEpoch(endUTC.getYear(), 0, 0, 0, 0), timeScale.duration); - endUTC = new UTCEpoch(endUTC.getYear(), 0, 0, 0, 0); - break; - case DAY: - beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), 0, 0, 0); - endUTC = utcSystem.add(new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), 0, 0, 0), - timeScale.duration); - endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), 0, 0, 0); - break; - case HOUR: - beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), beginUTC.getHour(), 0, 0); - endUTC = - utcSystem.add(new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), 0, 0), - timeScale.duration); - endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), 0, 0); - break; - case MINUTE: - beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), beginUTC.getHour(), - beginUTC.getMin(), 0); - endUTC = utcSystem.add( - new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), endUTC.getMin(), 0), - timeScale.duration); - endUTC = - new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), endUTC.getMin(), 0); - break; - case SECOND: - beginUTC = new UTCEpoch(beginUTC.getYear(), beginUTC.getDoy(), beginUTC.getHour(), - beginUTC.getMin(), Math.floor(beginUTC.getSec())); - endUTC = utcSystem.add(new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), - endUTC.getMin(), Math.floor(endUTC.getSec())), timeScale.duration); - endUTC = new UTCEpoch(endUTC.getYear(), endUTC.getDoy(), endUTC.getHour(), endUTC.getMin(), - Math.floor(endUTC.getSec())); - break; - case MILLISECOND: - beginUTC = beginUTC.createValueRoundedToMillisecs(); - endUTC = endUTC.createValueRoundedToMillisecs(); - default: - break; - } - - // put axis in units of the appropriate timescale - JulianDate roundedBegin = JulianDate.fromUTCEpoch(beginUTC); - JulianDate roundedEnd = JulianDate.fromUTCEpoch(endUTC); - min = 0; - max = 86400 * (roundedEnd.getDate() - roundedBegin.getDate()) / timeScale.duration; - - AxisRange range = new AxisRange(min, max); - TickMarks tickMarks = new TickMarks(range, false); - - NavigableSet majorTicks = new TreeSet<>(); - for (Double tick : tickMarks.getMajorTicks()) { - JulianDate jd = new JulianDate(roundedBegin.getDate() + tick * timeScale.duration / 86400); - majorTicks.add(ta.convertToET(jd.toUTCEpoch())); - } - - NavigableSet minorTicks = new TreeSet<>(); - for (Double tick : tickMarks.getMinorTicks()) { - JulianDate jd = new JulianDate(roundedBegin.getDate() + tick * timeScale.duration / 86400); - minorTicks.add(ta.convertToET(jd.toUTCEpoch())); - } - - this.tickMarks = new TickMarks(new AxisRange(ta.convertToET(roundedBegin.toUTCEpoch()), - ta.convertToET(roundedEnd.toUTCEpoch())), false); - tickMarks.setMajorTicks(majorTicks); - tickMarks.setMinorTicks(minorTicks); - this.tickMarks = tickMarks; - - setTickLabelFunction(ISOC); - - /*- - // put axis in units of the appropriate timescale - double roundedBegin = ta.convertToET(beginUTC); - double roundedEnd = ta.convertToET(endUTC); - min = 0; - max = (roundedEnd - roundedBegin) / timeScale.duration; - - AxisRange range = new AxisRange(min, max); - TickMarks tickMarks = new TickMarks(range, false); - - NavigableSet majorTicks = new TreeSet<>(); - for (Double tick : tickMarks.getMajorTicks()) { - majorTicks.add(roundedBegin + tick * timeScale.duration); - } - - NavigableSet minorTicks = new TreeSet<>(); - for (Double tick : tickMarks.getMinorTicks()) { - minorTicks.add(roundedBegin + tick * timeScale.duration); - } - - this.tickMarks = new TickMarks(new AxisRange(roundedBegin, roundedEnd), false); - tickMarks.setMajorTicks(majorTicks); - tickMarks.setMinorTicks(minorTicks); - - this.tickMarks = tickMarks; - */ - - } - } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/Projection.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/Projection.java index 4a1a054..a1eb45c 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/Projection.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/Projection.java @@ -31,141 +31,138 @@ import picante.math.vectorspace.UnwritableVectorIJK; public abstract class Projection { - protected final int w, h; - protected boolean isWrapAround; + protected final int w, h; + protected boolean isWrapAround; - protected boolean rotate; - private UnwritableMatrixIJK rotateXYZ; - private UnwritableMatrixIJK rotateZYX; + protected boolean rotate; + private UnwritableMatrixIJK rotateXYZ; + private UnwritableMatrixIJK rotateZYX; - public Projection(int w, int h) { - this(w, h, new LatitudinalVector(1, 0, 0), 0); - } - - public Projection(int w, int h, LatitudinalVector centerPoint) { - this(w, h, centerPoint, 0); - } - - public Projection(int w, int h, LatitudinalVector centerPoint, double rotation) { - this.w = w; - this.h = h; - this.rotate = - (centerPoint.getLatitude() != 0 || centerPoint.getLongitude() != 0 || rotation != 0); - if (rotate) { - setXYZRotationMatrix(rotation, centerPoint.getLatitude(), -centerPoint.getLongitude()); - setZYXRotationMatrix(-rotation, -centerPoint.getLatitude(), centerPoint.getLongitude()); - } - isWrapAround = false; - } - - public int getWidth() { - return w; - } - - public int getHeight() { - return h; - } - - public boolean isWrapAround() { - return isWrapAround; - } - - protected void setXYZRotationMatrix(double rotX, double rotY, double rotZ) { - rotateXYZ = MatrixIJK.IDENTITY; - if (rotX == 0 && rotY == 0 && rotZ == 0) { - return; + public Projection(int w, int h) { + this(w, h, new LatitudinalVector(1, 0, 0), 0); } - double cosx = Math.cos(rotX); - double cosy = Math.cos(rotY); - double cosz = Math.cos(rotZ); - double sinx = Math.sin(rotX); - double siny = Math.sin(rotY); - double sinz = Math.sin(rotZ); - - double ii = cosy * cosz; - double ij = sinx * siny * cosz + cosx * sinz; - double ik = -cosx * siny * cosz + sinx * sinz; - double ji = -cosy * sinz; - double jj = -sinx * siny * sinz + cosx * cosz; - double jk = cosx * siny * sinz + sinx * cosz; - double ki = siny; - double kj = -sinx * cosy; - double kk = cosx * cosy; - - rotateXYZ = new UnwritableMatrixIJK(ii, ji, ki, ij, jj, kj, ik, jk, kk); - } - - protected void setZYXRotationMatrix(double rotX, double rotY, double rotZ) { - rotateZYX = MatrixIJK.IDENTITY; - if (rotX == 0 && rotY == 0 && rotZ == 0) { - return; + public Projection(int w, int h, LatitudinalVector centerPoint) { + this(w, h, centerPoint, 0); } - double cosx = Math.cos(rotX); - double cosy = Math.cos(rotY); - double cosz = Math.cos(rotZ); - double sinx = Math.sin(rotX); - double siny = Math.sin(rotY); - double sinz = Math.sin(rotZ); + public Projection(int w, int h, LatitudinalVector centerPoint, double rotation) { + this.w = w; + this.h = h; + this.rotate = (centerPoint.getLatitude() != 0 || centerPoint.getLongitude() != 0 || rotation != 0); + if (rotate) { + setXYZRotationMatrix(rotation, centerPoint.getLatitude(), -centerPoint.getLongitude()); + setZYXRotationMatrix(-rotation, -centerPoint.getLatitude(), centerPoint.getLongitude()); + } + isWrapAround = false; + } - double ii = cosy * cosz; - double ij = cosy * sinz; - double ik = -siny; - double ji = -cosx * sinz + sinx * siny * cosz; - double jj = sinx * siny * sinz + cosx * cosz; - double jk = sinx * cosy; - double ki = cosx * siny * cosz + sinx * sinz; - double kj = -sinx * cosz + cosx * siny * sinz; - double kk = cosx * cosy; + public int getWidth() { + return w; + } - rotateZYX = new UnwritableMatrixIJK(ii, ji, ki, ij, jj, kj, ik, jk, kk); - } + public int getHeight() { + return h; + } - public LatitudinalVector rotateXYZ(double lat, double lon) { - return rotateXYZ(new LatitudinalVector(1, lat, lon)); - } + public boolean isWrapAround() { + return isWrapAround; + } - public LatitudinalVector rotateXYZ(LatitudinalVector point) { - UnwritableVectorIJK xyz = CoordConverters.convert(point); - UnwritableVectorIJK rotated = rotateXYZ.mxv(xyz); + protected void setXYZRotationMatrix(double rotX, double rotY, double rotZ) { + rotateXYZ = MatrixIJK.IDENTITY; + if (rotX == 0 && rotY == 0 && rotZ == 0) { + return; + } - return CoordConverters.convertToLatitudinal(rotated); - } + double cosx = Math.cos(rotX); + double cosy = Math.cos(rotY); + double cosz = Math.cos(rotZ); + double sinx = Math.sin(rotX); + double siny = Math.sin(rotY); + double sinz = Math.sin(rotZ); - public LatitudinalVector rotateZYX(double lat, double lon) { - return rotateZYX(new LatitudinalVector(1, lat, lon)); - } + double ii = cosy * cosz; + double ij = sinx * siny * cosz + cosx * sinz; + double ik = -cosx * siny * cosz + sinx * sinz; + double ji = -cosy * sinz; + double jj = -sinx * siny * sinz + cosx * cosz; + double jk = cosx * siny * sinz + sinx * cosz; + double ki = siny; + double kj = -sinx * cosy; + double kk = cosx * cosy; - public LatitudinalVector rotateZYX(LatitudinalVector point) { - UnwritableVectorIJK xyz = CoordConverters.convert(point); - UnwritableVectorIJK rotated = rotateZYX.mxv(xyz); + rotateXYZ = new UnwritableMatrixIJK(ii, ji, ki, ij, jj, kj, ik, jk, kk); + } - return CoordConverters.convertToLatitudinal(rotated); - } + protected void setZYXRotationMatrix(double rotX, double rotY, double rotZ) { + rotateZYX = MatrixIJK.IDENTITY; + if (rotX == 0 && rotY == 0 && rotZ == 0) { + return; + } - public LatitudinalVector pixelToSpherical(double x, double y) { - return pixelToSpherical(new Point2D.Double(x, y)); - } + double cosx = Math.cos(rotX); + double cosy = Math.cos(rotY); + double cosz = Math.cos(rotZ); + double sinx = Math.sin(rotX); + double siny = Math.sin(rotY); + double sinz = Math.sin(rotZ); - public Point2D sphericalToPixel(double lat, double lon) { - return sphericalToPixel(new LatitudinalVector(1, lat, lon)); - } + double ii = cosy * cosz; + double ij = cosy * sinz; + double ik = -siny; + double ji = -cosx * sinz + sinx * siny * cosz; + double jj = sinx * siny * sinz + cosx * cosz; + double jk = sinx * cosy; + double ki = cosx * siny * cosz + sinx * sinz; + double kj = -sinx * cosz + cosx * siny * sinz; + double kk = cosx * cosy; - public abstract LatitudinalVector pixelToSpherical(Point2D xy); + rotateZYX = new UnwritableMatrixIJK(ii, ji, ki, ij, jj, kj, ik, jk, kk); + } - public abstract Point2D sphericalToPixel(LatitudinalVector latLon); + public LatitudinalVector rotateXYZ(double lat, double lon) { + return rotateXYZ(new LatitudinalVector(1, lat, lon)); + } - /** - * - * @return angle between center pixel and one pixel to the right - */ - public double radiansPerPixel() { - UnwritableVectorIJK center = - CoordConverters.convert(pixelToSpherical(new Point2D.Double(w / 2., h / 2.))); - UnwritableVectorIJK neighbor = - CoordConverters.convert(pixelToSpherical(new Point2D.Double(w / 2. + 1, h / 2.))); - return center.getSeparation(neighbor); - } + public LatitudinalVector rotateXYZ(LatitudinalVector point) { + UnwritableVectorIJK xyz = CoordConverters.convert(point); + UnwritableVectorIJK rotated = rotateXYZ.mxv(xyz); + return CoordConverters.convertToLatitudinal(rotated); + } + + public LatitudinalVector rotateZYX(double lat, double lon) { + return rotateZYX(new LatitudinalVector(1, lat, lon)); + } + + public LatitudinalVector rotateZYX(LatitudinalVector point) { + UnwritableVectorIJK xyz = CoordConverters.convert(point); + UnwritableVectorIJK rotated = rotateZYX.mxv(xyz); + + return CoordConverters.convertToLatitudinal(rotated); + } + + public LatitudinalVector pixelToSpherical(double x, double y) { + return pixelToSpherical(new Point2D.Double(x, y)); + } + + public Point2D sphericalToPixel(double lat, double lon) { + return sphericalToPixel(new LatitudinalVector(1, lat, lon)); + } + + public abstract LatitudinalVector pixelToSpherical(Point2D xy); + + public abstract Point2D sphericalToPixel(LatitudinalVector latLon); + + /** + * + * @return angle between center pixel and one pixel to the right + */ + public double radiansPerPixel() { + UnwritableVectorIJK center = CoordConverters.convert(pixelToSpherical(new Point2D.Double(w / 2., h / 2.))); + UnwritableVectorIJK neighbor = + CoordConverters.convert(pixelToSpherical(new Point2D.Double(w / 2. + 1, h / 2.))); + return center.getSeparation(neighbor); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionMollweide.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionMollweide.java index b5b5b9c..1e5899e 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionMollweide.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionMollweide.java @@ -27,79 +27,69 @@ import picante.math.coords.LatitudinalVector; public class ProjectionMollweide extends Projection { - private final double radius; + private final double radius; - public ProjectionMollweide(int w, int h, LatitudinalVector centerPoint, double rotation) { - super(w, h, centerPoint, rotation); - radius = 0.5; - } - - public ProjectionMollweide(int w, int h, LatitudinalVector centerPoint) { - this(w, h, centerPoint, 0); - } - - public ProjectionMollweide(int w, int h) { - this(w, h, new LatitudinalVector(1, 0, 0), 0); - } - - @Override - public LatitudinalVector pixelToSpherical(Point2D xy) { - double X = 2.0 * xy.getX() / w - 1; - double Y = 1 - 2.0 * xy.getY() / h; - - double arg = Y / radius; - if (Math.abs(arg) > 1) - return null; - double theta = Math.asin(arg); - - arg = (2 * theta + Math.sin(2 * theta)) / Math.PI; - if (Math.abs(arg) > 1) - return null; - double lat = Math.asin(arg); - - double lon; - if (Math.abs(theta) == Math.PI) - lon = 0; - else { - lon = Math.PI * X / (2 * radius * Math.cos(theta)); - if (Math.abs(lon) > Math.PI) - return null; + public ProjectionMollweide(int w, int h, LatitudinalVector centerPoint, double rotation) { + super(w, h, centerPoint, rotation); + radius = 0.5; } - LatitudinalVector ll = new LatitudinalVector(1, lat, lon); - if (rotate) - ll = rotateXYZ(ll); - - return ll; - } - - @Override - public Point2D sphericalToPixel(LatitudinalVector latLon) { - LatitudinalVector ll = new LatitudinalVector(1, latLon.getLatitude(), latLon.getLongitude()); - if (rotate) - ll = rotateZYX(ll); - - double theta = ll.getLatitude(); - double del_theta = 1; - while (Math.abs(del_theta) > 1e-5) { - del_theta = -((theta + Math.sin(theta) - Math.PI * Math.sin(ll.getLatitude())) - / (1 + Math.cos(theta))); - theta += del_theta; + public ProjectionMollweide(int w, int h, LatitudinalVector centerPoint) { + this(w, h, centerPoint, 0); } - theta /= 2; - double X = (2 * radius / Math.PI) * ll.getLongitude() * Math.cos(theta); - double Y = radius * Math.sin(theta); + public ProjectionMollweide(int w, int h) { + this(w, h, new LatitudinalVector(1, 0, 0), 0); + } - double x = (X + 1) * w / 2; - if (x < 0 || x >= w) - return null; + @Override + public LatitudinalVector pixelToSpherical(Point2D xy) { + double X = 2.0 * xy.getX() / w - 1; + double Y = 1 - 2.0 * xy.getY() / h; - double y = h / 2. * (1 - Y); - if (y < 0 || y >= h) - return null; + double arg = Y / radius; + if (Math.abs(arg) > 1) return null; + double theta = Math.asin(arg); - return new Point2D.Double(x, y); - } + arg = (2 * theta + Math.sin(2 * theta)) / Math.PI; + if (Math.abs(arg) > 1) return null; + double lat = Math.asin(arg); + double lon; + if (Math.abs(theta) == Math.PI) lon = 0; + else { + lon = Math.PI * X / (2 * radius * Math.cos(theta)); + if (Math.abs(lon) > Math.PI) return null; + } + + LatitudinalVector ll = new LatitudinalVector(1, lat, lon); + if (rotate) ll = rotateXYZ(ll); + + return ll; + } + + @Override + public Point2D sphericalToPixel(LatitudinalVector latLon) { + LatitudinalVector ll = new LatitudinalVector(1, latLon.getLatitude(), latLon.getLongitude()); + if (rotate) ll = rotateZYX(ll); + + double theta = ll.getLatitude(); + double del_theta = 1; + while (Math.abs(del_theta) > 1e-5) { + del_theta = -((theta + Math.sin(theta) - Math.PI * Math.sin(ll.getLatitude())) / (1 + Math.cos(theta))); + theta += del_theta; + } + theta /= 2; + + double X = (2 * radius / Math.PI) * ll.getLongitude() * Math.cos(theta); + double Y = radius * Math.sin(theta); + + double x = (X + 1) * w / 2; + if (x < 0 || x >= w) return null; + + double y = h / 2. * (1 - Y); + if (y < 0 || y >= h) return null; + + return new Point2D.Double(x, y); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionOrthographic.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionOrthographic.java index 1259fe2..96c50c8 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionOrthographic.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionOrthographic.java @@ -27,127 +27,116 @@ import picante.math.coords.LatitudinalVector; public class ProjectionOrthographic extends Projection { - private double P, Pm1, Pp1, PPm1, Pm1sq; - private double dispScale; - private double radius; - private final double range; + private double P, Pm1, Pp1, PPm1, Pm1sq; + private double dispScale; + private double radius; + private final double range; - public ProjectionOrthographic(int w, int h) { - this(w, h, new LatitudinalVector(1, 0, 0)); - } - - public ProjectionOrthographic(int w, int h, LatitudinalVector centerPoint) { - this(w, h, centerPoint, 0); - - } - - public ProjectionOrthographic(int w, int h, LatitudinalVector centerPoint, double rotation) { - super(w, h, centerPoint, rotation); - isWrapAround = false; - - radius = 0.5; - range = 1000; - - dispScale = radius * h; - - setRange(range); - } - - /** - * - * @param radius radius of the rendered disk as a fraction of the image height. Default is 0.5. - */ - public void setRadius(double radius) { - this.radius = radius; - dispScale = radius * h; - setRange(range); - } - - public void setRange(double range) { - P = range; - Pp1 = (P + 1); - Pm1 = (P - 1); - PPm1 = P * Pm1; - Pm1sq = Pm1 * Pm1; - - dispScale *= Math.sqrt(Pp1 / Pm1); - } - - @Override - public LatitudinalVector pixelToSpherical(Point2D xy) { - final double X = (xy.getX() - w / 2.) / dispScale; - final double Y = (h / 2. - xy.getY()) / dispScale; - - final double rho2 = X * X + Y * Y; - if (rho2 > 1) - return null; - - final double rho = Math.sqrt(rho2); - - double lat; - double lon; - if (rho == 0) { - lat = 0; - lon = 0; - } else { - double arg = Pm1 * (Pm1 - rho2 * Pp1); - if (arg < 0) - return null; - - final double N = rho * (PPm1 - Math.sqrt(arg)); - final double D = (Pm1sq + rho2); - - final double sinc = N / D; - final double cosc = Math.sqrt(1 - sinc * sinc); - - arg = Y * sinc / rho; - if (Math.abs(arg) > 1) - return null; - - lat = Math.asin(arg); - lon = Math.atan2(X * sinc, rho * cosc); + public ProjectionOrthographic(int w, int h) { + this(w, h, new LatitudinalVector(1, 0, 0)); } - LatitudinalVector ll = new LatitudinalVector(1, lat, lon); - if (rotate) - ll = rotateXYZ(ll); - - return ll; - } - - @Override - public Point2D sphericalToPixel(LatitudinalVector latLon) { - LatitudinalVector ll = new LatitudinalVector(1, latLon.getLatitude(), latLon.getLongitude()); - if (rotate) - ll = rotateZYX(ll); - - double lat = ll.getLatitude(); - double lon = ll.getLongitude(); - - final double cosc = Math.cos(lat) * Math.cos(lon); - if (cosc < 0) - return null; - - final double k = (P - 1) / (P - cosc); - - final double X = k * Math.cos(lat) * Math.sin(lon); - final double Y = k * Math.sin(lat); - - double x = X * dispScale + w / 2.; - if (x < 0 || x >= w) - return null; - - double y = h / 2. - Y * dispScale; - if (y < 0 || y >= h) - return null; - - if (P * cosc < 1) { - double dist = Math.sqrt(x * x + y * y); - if (dist < dispScale) - return null; + public ProjectionOrthographic(int w, int h, LatitudinalVector centerPoint) { + this(w, h, centerPoint, 0); } - return new Point2D.Double(x, y); - } + public ProjectionOrthographic(int w, int h, LatitudinalVector centerPoint, double rotation) { + super(w, h, centerPoint, rotation); + isWrapAround = false; + radius = 0.5; + range = 1000; + + dispScale = radius * h; + + setRange(range); + } + + /** + * + * @param radius radius of the rendered disk as a fraction of the image height. Default is 0.5. + */ + public void setRadius(double radius) { + this.radius = radius; + dispScale = radius * h; + setRange(range); + } + + public void setRange(double range) { + P = range; + Pp1 = (P + 1); + Pm1 = (P - 1); + PPm1 = P * Pm1; + Pm1sq = Pm1 * Pm1; + + dispScale *= Math.sqrt(Pp1 / Pm1); + } + + @Override + public LatitudinalVector pixelToSpherical(Point2D xy) { + final double X = (xy.getX() - w / 2.) / dispScale; + final double Y = (h / 2. - xy.getY()) / dispScale; + + final double rho2 = X * X + Y * Y; + if (rho2 > 1) return null; + + final double rho = Math.sqrt(rho2); + + double lat; + double lon; + if (rho == 0) { + lat = 0; + lon = 0; + } else { + double arg = Pm1 * (Pm1 - rho2 * Pp1); + if (arg < 0) return null; + + final double N = rho * (PPm1 - Math.sqrt(arg)); + final double D = (Pm1sq + rho2); + + final double sinc = N / D; + final double cosc = Math.sqrt(1 - sinc * sinc); + + arg = Y * sinc / rho; + if (Math.abs(arg) > 1) return null; + + lat = Math.asin(arg); + lon = Math.atan2(X * sinc, rho * cosc); + } + + LatitudinalVector ll = new LatitudinalVector(1, lat, lon); + if (rotate) ll = rotateXYZ(ll); + + return ll; + } + + @Override + public Point2D sphericalToPixel(LatitudinalVector latLon) { + LatitudinalVector ll = new LatitudinalVector(1, latLon.getLatitude(), latLon.getLongitude()); + if (rotate) ll = rotateZYX(ll); + + double lat = ll.getLatitude(); + double lon = ll.getLongitude(); + + final double cosc = Math.cos(lat) * Math.cos(lon); + if (cosc < 0) return null; + + final double k = (P - 1) / (P - cosc); + + final double X = k * Math.cos(lat) * Math.sin(lon); + final double Y = k * Math.sin(lat); + + double x = X * dispScale + w / 2.; + if (x < 0 || x >= w) return null; + + double y = h / 2. - Y * dispScale; + if (y < 0 || y >= h) return null; + + if (P * cosc < 1) { + double dist = Math.sqrt(x * x + y * y); + if (dist < dispScale) return null; + } + + return new Point2D.Double(x, y); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionRectangular.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionRectangular.java index eeb25cc..300d242 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionRectangular.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/projection/ProjectionRectangular.java @@ -29,81 +29,74 @@ import terrasaur.utils.saaPlotLib.canvas.axis.AxisY; public class ProjectionRectangular extends Projection { - /** - * coordinates of upper left corner - */ - private final LatitudinalVector startPos; - private double delLat; - private double delLon; + /** + * coordinates of upper left corner + */ + private final LatitudinalVector startPos; - public ProjectionRectangular(int w, int h) { - this(w, h, new LatitudinalVector(1, 0, 0)); - } + private double delLat; + private double delLon; - public ProjectionRectangular(int w, int h, LatitudinalVector centerPoint) { - super(w, h, centerPoint); - isWrapAround = true; - - startPos = new LatitudinalVector(1.0, Math.PI / 2, -Math.PI + centerPoint.getLongitude()); - delLat = Math.PI / h; - delLon = 2 * Math.PI / w; - } - - /** - * For a subset of the entire map. - * - * @param w width - * @param h height - * @param xAxis in radians - * @param yAxis in radians - * @param rotation rotation angle - */ - public ProjectionRectangular(int w, int h, AxisX xAxis, AxisY yAxis, double rotation) { - super(w, h); - - LatitudinalVector centerPoint = - new LatitudinalVector(1, yAxis.getRange().getMiddle(), xAxis.getRange().getMiddle()); - - this.rotate = - (centerPoint.getLatitude() != 0 || centerPoint.getLongitude() != 0 || rotation != 0); - if (rotate) { - setXYZRotationMatrix(rotation, centerPoint.getLatitude(), -centerPoint.getLongitude()); - setZYXRotationMatrix(-rotation, -centerPoint.getLatitude(), centerPoint.getLongitude()); + public ProjectionRectangular(int w, int h) { + this(w, h, new LatitudinalVector(1, 0, 0)); } - isWrapAround = true; + public ProjectionRectangular(int w, int h, LatitudinalVector centerPoint) { + super(w, h, centerPoint); + isWrapAround = true; - startPos = new LatitudinalVector(1.0, yAxis.getRange().getMax(), xAxis.getRange().getMin()); - delLat = yAxis.getRange().getLength() / h; - if (yAxis.getRange().isDecreasing()) - delLat *= -1; - delLon = xAxis.getRange().getLength() / w; - if (xAxis.getRange().isDecreasing()) - delLon *= -1; - } + startPos = new LatitudinalVector(1.0, Math.PI / 2, -Math.PI + centerPoint.getLongitude()); + delLat = Math.PI / h; + delLon = 2 * Math.PI / w; + } - @Override - public LatitudinalVector pixelToSpherical(Point2D xy) { - double lat = startPos.getLatitude() - xy.getY() * delLat; - double lon = xy.getX() * delLon + startPos.getLongitude(); - if (Math.abs(lat) > Math.PI / 2) - return null; - return new LatitudinalVector(1, lat, lon); - } + /** + * For a subset of the entire map. + * + * @param w width + * @param h height + * @param xAxis in radians + * @param yAxis in radians + * @param rotation rotation angle + */ + public ProjectionRectangular(int w, int h, AxisX xAxis, AxisY yAxis, double rotation) { + super(w, h); - @Override - public Point2D.Double sphericalToPixel(LatitudinalVector latLon) { - double x = (latLon.getLongitude() - startPos.getLongitude()) / delLon; - double y = (startPos.getLatitude() - latLon.getLatitude()) / delLat; - if (y < 0) - y = 0; - if (y >= h) - y = h - 1; - while (x < 0) - x += w; - while (x >= w) - x -= w; - return new Point2D.Double(x, y); - } + LatitudinalVector centerPoint = new LatitudinalVector( + 1, yAxis.getRange().getMiddle(), xAxis.getRange().getMiddle()); + this.rotate = (centerPoint.getLatitude() != 0 || centerPoint.getLongitude() != 0 || rotation != 0); + if (rotate) { + setXYZRotationMatrix(rotation, centerPoint.getLatitude(), -centerPoint.getLongitude()); + setZYXRotationMatrix(-rotation, -centerPoint.getLatitude(), centerPoint.getLongitude()); + } + + isWrapAround = true; + + startPos = new LatitudinalVector( + 1.0, yAxis.getRange().getMax(), xAxis.getRange().getMin()); + delLat = yAxis.getRange().getLength() / h; + if (yAxis.getRange().isDecreasing()) delLat *= -1; + delLon = xAxis.getRange().getLength() / w; + if (xAxis.getRange().isDecreasing()) delLon *= -1; + } + + @Override + public LatitudinalVector pixelToSpherical(Point2D xy) { + double lat = startPos.getLatitude() - xy.getY() * delLat; + double lon = xy.getX() * delLon + startPos.getLongitude(); + if (Math.abs(lat) > Math.PI / 2) return null; + return new LatitudinalVector(1, lat, lon); + } + + @Override + public Point2D.Double sphericalToPixel(LatitudinalVector latLon) { + double x = (latLon.getLongitude() - startPos.getLongitude()) / delLon; + double y = (startPos.getLatitude() - latLon.getLatitude()) / delLat; + if (y < 0) y = 0; + if (y >= h) y = h - 1; + while (x < 0) x += w; + while (x >= w) x -= w; + return new Point2D.Double(x, y); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Asterisk.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Asterisk.java index 5dab3a6..77b6534 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Asterisk.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Asterisk.java @@ -29,22 +29,21 @@ import java.awt.geom.Line2D; public class Asterisk extends Symbol { - public Asterisk() { - super(); - } - - @Override - public void draw(Graphics2D g, double x, double y) { - Graphics2D gg = (Graphics2D) g.create(); - - Shape shape = new Line2D.Double(x - size / 2, y, x + size / 2, y); - - for (int i = 0; i < 8; i++) { - AffineTransform at = AffineTransform.getRotateInstance(i * Math.toRadians(45) + rotate, x, y); - gg.draw(at.createTransformedShape(shape)); + public Asterisk() { + super(); } - gg.dispose(); - } + @Override + public void draw(Graphics2D g, double x, double y) { + Graphics2D gg = (Graphics2D) g.create(); + Shape shape = new Line2D.Double(x - size / 2, y, x + size / 2, y); + + for (int i = 0; i < 8; i++) { + AffineTransform at = AffineTransform.getRotateInstance(i * Math.toRadians(45) + rotate, x, y); + gg.draw(at.createTransformedShape(shape)); + } + + gg.dispose(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Circle.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Circle.java index 2d46943..ce9b604 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Circle.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Circle.java @@ -28,20 +28,18 @@ import java.awt.geom.Ellipse2D; public class Circle extends Symbol { - public Circle() { - super(); - } + public Circle() { + super(); + } - @Override - public void draw(Graphics2D g, double x, double y) { - Graphics2D gg = (Graphics2D) g.create(); + @Override + public void draw(Graphics2D g, double x, double y) { + Graphics2D gg = (Graphics2D) g.create(); - Shape shape = new Ellipse2D.Double(x - size / 2, y - size / 2, size, size); - gg.draw(shape); - if (fill) - gg.fill(shape); - - gg.dispose(); - } + Shape shape = new Ellipse2D.Double(x - size / 2, y - size / 2, size, size); + gg.draw(shape); + if (fill) gg.fill(shape); + gg.dispose(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Cross.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Cross.java index 66fa69c..3c8602e 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Cross.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Cross.java @@ -29,22 +29,21 @@ import java.awt.geom.Line2D; public class Cross extends Symbol { - public Cross() { - super(); - } - - @Override - public void draw(Graphics2D g, double x, double y) { - Graphics2D gg = (Graphics2D) g.create(); - - Shape shape = new Line2D.Double(x - size / 2, y, x + size / 2, y); - - for (int i = 0; i < 4; i++) { - AffineTransform at = AffineTransform.getRotateInstance(i * Math.toRadians(90) + rotate, x, y); - gg.draw(at.createTransformedShape(shape)); + public Cross() { + super(); } - gg.dispose(); - } + @Override + public void draw(Graphics2D g, double x, double y) { + Graphics2D gg = (Graphics2D) g.create(); + Shape shape = new Line2D.Double(x - size / 2, y, x + size / 2, y); + + for (int i = 0; i < 4; i++) { + AffineTransform at = AffineTransform.getRotateInstance(i * Math.toRadians(90) + rotate, x, y); + gg.draw(at.createTransformedShape(shape)); + } + + gg.dispose(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Square.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Square.java index 2b55fe5..334de25 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Square.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Square.java @@ -29,30 +29,28 @@ import java.awt.geom.Path2D; public class Square extends Symbol { - public Square() { - super(); - } + public Square() { + super(); + } - @Override - public void draw(Graphics2D g, double x, double y) { - Graphics2D gg = (Graphics2D) g.create(); + @Override + public void draw(Graphics2D g, double x, double y) { + Graphics2D gg = (Graphics2D) g.create(); - Path2D.Double p = new Path2D.Double(); - p.moveTo(x - size / 2, y + size / 2); - p.lineTo(x + size / 2, y + size / 2); - p.lineTo(x + size / 2, y - size / 2); - p.lineTo(x - size / 2, y - size / 2); - p.closePath(); + Path2D.Double p = new Path2D.Double(); + p.moveTo(x - size / 2, y + size / 2); + p.lineTo(x + size / 2, y + size / 2); + p.lineTo(x + size / 2, y - size / 2); + p.lineTo(x - size / 2, y - size / 2); + p.closePath(); - AffineTransform at = AffineTransform.getRotateInstance(rotate, x, y); - Shape s = at.createTransformedShape(p); + AffineTransform at = AffineTransform.getRotateInstance(rotate, x, y); + Shape s = at.createTransformedShape(p); - gg.draw(s); + gg.draw(s); - if (fill) - gg.fill(s); - - gg.dispose(); - } + if (fill) gg.fill(s); + gg.dispose(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Symbol.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Symbol.java index accbdda..51d44ac 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Symbol.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Symbol.java @@ -28,67 +28,67 @@ import java.awt.geom.Point2D; public abstract class Symbol { - public static final double DEFAULT_SIZE = 8; + public static final double DEFAULT_SIZE = 8; - protected double size; - protected boolean fill; - protected double rotate; + protected double size; + protected boolean fill; + protected double rotate; - public Symbol() { - size = DEFAULT_SIZE; - fill = false; - rotate = 0; - } + public Symbol() { + size = DEFAULT_SIZE; + fill = false; + rotate = 0; + } - public Symbol setFill(boolean fill) { - this.fill = fill; - return this; - } + public Symbol setFill(boolean fill) { + this.fill = fill; + return this; + } - public double getSize() { - return size; - } + public double getSize() { + return size; + } - public Symbol setSize(double size) { - this.size = size; - return this; - } + public Symbol setSize(double size) { + this.size = size; + return this; + } - public Symbol setRotate(double rotateDeg) { - this.rotate = Math.toRadians(rotateDeg); - return this; - } + public Symbol setRotate(double rotateDeg) { + this.rotate = Math.toRadians(rotateDeg); + return this; + } - public abstract void draw(Graphics2D g, double x, double y); + public abstract void draw(Graphics2D g, double x, double y); - public void drawError(Graphics2D g, double x, double y, Point2D xError, Point2D yError) { - Graphics2D gg = (Graphics2D) g.create(); + public void drawError(Graphics2D g, double x, double y, Point2D xError, Point2D yError) { + Graphics2D gg = (Graphics2D) g.create(); - if (xError != null) gg.draw(new Line2D.Double(xError.getX(), y, xError.getY(), y)); + if (xError != null) gg.draw(new Line2D.Double(xError.getX(), y, xError.getY(), y)); - if (yError != null) gg.draw(new Line2D.Double(x, yError.getX(), x, yError.getY())); + if (yError != null) gg.draw(new Line2D.Double(x, yError.getX(), x, yError.getY())); - gg.dispose(); - } + gg.dispose(); + } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (fill ? 1231 : 1237); - result = prime * result + Double.hashCode(rotate); - result = prime * result + Double.hashCode(size); - return result; - } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (fill ? 1231 : 1237); + result = prime * result + Double.hashCode(rotate); + result = prime * result + Double.hashCode(size); + return result; + } - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - Symbol other = (Symbol) obj; - if (fill != other.fill) return false; - if (Double.doubleToLongBits(rotate) != Double.doubleToLongBits(other.rotate)) return false; - return Double.doubleToLongBits(size) == Double.doubleToLongBits(other.size); - } + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Symbol other = (Symbol) obj; + if (fill != other.fill) return false; + if (Double.doubleToLongBits(rotate) != Double.doubleToLongBits(other.rotate)) return false; + return Double.doubleToLongBits(size) == Double.doubleToLongBits(other.size); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Triangle.java b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Triangle.java index 9e916d8..09f8afa 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Triangle.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/canvas/symbol/Triangle.java @@ -29,31 +29,29 @@ import java.awt.geom.Path2D; public class Triangle extends Symbol { - public Triangle() { - super(); - } + public Triangle() { + super(); + } - @Override - public void draw(Graphics2D g, double x, double y) { - Graphics2D gg = (Graphics2D) g.create(); + @Override + public void draw(Graphics2D g, double x, double y) { + Graphics2D gg = (Graphics2D) g.create(); - final double side = 2 * size / Math.sqrt(3.); + final double side = 2 * size / Math.sqrt(3.); - Path2D.Double p = new Path2D.Double(); - p.moveTo(x - side / 2, y + size / 2); - p.lineTo(x + side / 2, y + size / 2); - p.lineTo(x, y - size / 2); - p.closePath(); + Path2D.Double p = new Path2D.Double(); + p.moveTo(x - side / 2, y + size / 2); + p.lineTo(x + side / 2, y + size / 2); + p.lineTo(x, y - size / 2); + p.closePath(); - AffineTransform at = AffineTransform.getRotateInstance(rotate, x, y); - Shape s = at.createTransformedShape(p); + AffineTransform at = AffineTransform.getRotateInstance(rotate, x, y); + Shape s = at.createTransformedShape(p); - gg.draw(s); + gg.draw(s); - if (fill) - gg.fill(s); - - gg.dispose(); - } + if (fill) gg.fill(s); + gg.dispose(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorBar.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorBar.java index 75cb2ff..838b5d9 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorBar.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorBar.java @@ -29,17 +29,16 @@ import org.immutables.value.Value; @Value.Immutable public abstract class ColorBar { - public abstract Rectangle rect(); + public abstract Rectangle rect(); - public abstract ColorRamp ramp(); + public abstract ColorRamp ramp(); - public abstract int numTicks(); + public abstract int numTicks(); - public abstract Function tickFunction(); - - @Value.Default - public boolean log() { - return false; - } + public abstract Function tickFunction(); + @Value.Default + public boolean log() { + return false; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorRamp.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorRamp.java index f6a592e..59b451e 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorRamp.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/ColorRamp.java @@ -45,320 +45,323 @@ import terrasaur.utils.saaPlotLib.colorMaps.tables.ScientificColourMaps6; @Value.Immutable public abstract class ColorRamp { - private static final int NUM_COLORS = 256; + private static final int NUM_COLORS = 256; - /** - * @param type ColorRamp type - * @param min minimum value - * @param max maximum value - * @return ColorRamp - */ - public static ColorRamp create(TYPE type, double min, double max) { - return ImmutableColorRamp.builder().min(min).max(max).colors(type.getColors()).build(); - } - - /** - * - * @param min minimum value - * @param minColor color mapped to minimum - * @param midColor color mapped to middle - * @param max maximum value - * @param maxColor color mapped to maximum - * @return Color ramp linearly interpolated between minColor and midColor, and midColor and - * * maxColor. - */ - public static ColorRamp createBilinear( - double min, Color minColor, Color midColor, double max, Color maxColor) { - - int minRed = minColor.getRed(); - int redRange = midColor.getRed() - minColor.getRed(); - int minGreen = minColor.getGreen(); - int greenRange = midColor.getGreen() - minColor.getGreen(); - int minBlue = minColor.getBlue(); - int blueRange = midColor.getBlue() - minColor.getBlue(); - - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS / 2; i++) { - - double frac = ((double) i) / (NUM_COLORS / 2. - 1); - - int red = (int) (minRed + frac * redRange); - int green = (int) (minGreen + frac * greenRange); - int blue = (int) (minBlue + frac * blueRange); - - colors.add(new Color(red, green, blue)); + /** + * @param type ColorRamp type + * @param min minimum value + * @param max maximum value + * @return ColorRamp + */ + public static ColorRamp create(TYPE type, double min, double max) { + return ImmutableColorRamp.builder() + .min(min) + .max(max) + .colors(type.getColors()) + .build(); } - minRed = midColor.getRed(); - redRange = maxColor.getRed() - midColor.getRed(); - minGreen = midColor.getGreen(); - greenRange = maxColor.getGreen() - midColor.getGreen(); - minBlue = midColor.getBlue(); - blueRange = maxColor.getBlue() - midColor.getBlue(); + /** + * + * @param min minimum value + * @param minColor color mapped to minimum + * @param midColor color mapped to middle + * @param max maximum value + * @param maxColor color mapped to maximum + * @return Color ramp linearly interpolated between minColor and midColor, and midColor and + * * maxColor. + */ + public static ColorRamp createBilinear(double min, Color minColor, Color midColor, double max, Color maxColor) { - for (int i = 0; i < NUM_COLORS / 2; i++) { + int minRed = minColor.getRed(); + int redRange = midColor.getRed() - minColor.getRed(); + int minGreen = minColor.getGreen(); + int greenRange = midColor.getGreen() - minColor.getGreen(); + int minBlue = minColor.getBlue(); + int blueRange = midColor.getBlue() - minColor.getBlue(); - double frac = ((double) i) / (NUM_COLORS / 2. - 1); + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS / 2; i++) { - int red = (int) (minRed + frac * redRange); - int green = (int) (minGreen + frac * greenRange); - int blue = (int) (minBlue + frac * blueRange); + double frac = ((double) i) / (NUM_COLORS / 2. - 1); - colors.add(new Color(red, green, blue)); - } - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } + int red = (int) (minRed + frac * redRange); + int green = (int) (minGreen + frac * greenRange); + int blue = (int) (minBlue + frac * blueRange); - /** - * - * @param min minimum - * @param minColor color mapped to minimum - * @param max maximum - * @param maxColor color mapped to maximum - * @return divergent color map, based on https://www.kennethmoreland.com/color-maps - */ - public static ColorRamp createDivergent(double min, Color minColor, double max, Color maxColor) { - DivergentColorRamp dcr = new DivergentColorRamp(); - return ImmutableColorRamp.builder() - .min(min) - .max(max) - .colors(dcr.generateColorMap(minColor, maxColor, NUM_COLORS)) - .build(); - } + colors.add(new Color(red, green, blue)); + } - /** - * Assign ColorRamp from an array of ints where red is in bits 16-23, green is in bits 8-15, and - * blue is in bits 0-7. - * - * @param min minimum - * @param max maximum - * @param values integer values - * @return color ramp - */ - public static ColorRamp createFromInt(double min, double max, int[] values) { - List colors = new ArrayList<>(); - for (int value : values) colors.add(new Color(value)); - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } + minRed = midColor.getRed(); + redRange = maxColor.getRed() - midColor.getRed(); + minGreen = midColor.getGreen(); + greenRange = maxColor.getGreen() - midColor.getGreen(); + minBlue = midColor.getBlue(); + blueRange = maxColor.getBlue() - midColor.getBlue(); - /** - * - * @param min minimum - * @param max maximum - * @param r red - * @param g green - * @param b blue - * @return colormap from arrays of red, green, and blue values. Each color array must have 256 values. - */ - public static ColorRamp createFromRGB(double min, double max, int[] r, int[] g, int[] b) { - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS; i++) colors.add(new Color(r[i], g[i], b[i])); - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } + for (int i = 0; i < NUM_COLORS / 2; i++) { - /** - * @param min minimum - * @param max maximum - * @return a greyscale ramp with the min value black and the max value white. - */ - public static ColorRamp createGreyscale(double min, double max) { - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS; i++) { - colors.add(new Color(i, i, i)); - } - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } + double frac = ((double) i) / (NUM_COLORS / 2. - 1); - /** - * @param min minimum - * @param max maximum - * @return a circular color ramp running from hue 0 to 360 with saturation and brightness set to - * 1. - */ - public static ColorRamp createHue(double min, double max) { - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS; i++) { - float h = ((float) i) / (NUM_COLORS - 1); + int red = (int) (minRed + frac * redRange); + int green = (int) (minGreen + frac * greenRange); + int blue = (int) (minBlue + frac * blueRange); - colors.add(Color.getHSBColor(h, 1.0f, 1.0f)); - } - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } - - /** - * - * @param min minimum - * @param minColor color mapped to minimum - * @param max maximum - * @param maxColor color mapped to maximum - * @return color ramp linearly interpolated between minColor and maxColor. - */ - public static ColorRamp createLinear(double min, Color minColor, double max, Color maxColor) { - - final int minRed = minColor.getRed(); - final int redRange = maxColor.getRed() - minColor.getRed(); - final int minGreen = minColor.getGreen(); - final int greenRange = maxColor.getGreen() - minColor.getGreen(); - final int minBlue = minColor.getBlue(); - final int blueRange = maxColor.getBlue() - minColor.getBlue(); - - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS; i++) { - - double frac = ((double) i) / (NUM_COLORS - 1); - - int red = (int) (minRed + frac * redRange); - int green = (int) (minGreen + frac * greenRange); - int blue = (int) (minBlue + frac * blueRange); - - colors.add(new Color(red, green, blue)); + colors.add(new Color(red, green, blue)); + } + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); } - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } - - /** - * @param min minimum - * @param max maximum - * @return a linear color ramp running from hue 240 (blue) to hue 0 degrees (red) with saturation - * and brightness set to 1. - */ - public static ColorRamp createLinear(double min, double max) { - List colors = new ArrayList<>(); - for (int i = 0; i < NUM_COLORS; i++) { - float h = (float) (2. / 3. * (1. - i / (NUM_COLORS - 1.))); - float s = 1.0f; - float b = 1.0f; - colors.add(Color.getHSBColor(h, s, b)); - } - return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); - } - - public abstract List colors(); - - /** set the lower limit color to black and the upper limit color to white. */ - @Value.Default - public boolean limitColors() { - return false; - } - - public abstract double max(); - - public abstract double min(); - - @Value.Default - public String title() { - return ""; - } - - /** - * @return a ramp with the lower limit color set to black and the upper limit color to white. - */ - public ColorRamp addLimitColors() { - return ImmutableColorRamp.builder().from(this).limitColors(true).build(); - } - - public ColorRamp addTitle(String title) { - return ImmutableColorRamp.builder().from(this).title(title).build(); - } - - /** - * @return a color ramp that is the reverse of this one - */ - public ColorRamp createReverse() { - List newColors = new ArrayList<>(colors()); - Collections.reverse(newColors); - return ImmutableColorRamp.builder().from(this).colors(newColors).build(); - } - - /** - * - * @param value input value - * @return input value mapped to a color. If Double.isFinite(value) is false a transparent Color is - * returned. - */ - public Color getColor(double value) { - - if (Double.isFinite(value)) { - double frac = (value - min()) / (max() - min()); - if (frac < 0) { - if (limitColors()) return Color.BLACK; - frac = 0; - } - if (frac > 1) { - if (limitColors()) return Color.WHITE; - frac = 1; - } - frac *= colors().size(); - return colors().get(Math.min((int) frac, colors().size() - 1)); - } else { - // transparent - return new Color(0, 0, 0, 0); - } - } - - public enum TYPE { - BATLOW(ScientificColourMaps6.BATLOW), - BERLIN(ScientificColourMaps6.BERLIN), - BGY(Colorcet.BGY), - BGYW(Colorcet.BGYW), - BJY(Colorcet.BJY), - BKR(Colorcet.BKR), - BKY(Colorcet.BKY), - BLUES(Colorcet.BLUES), - BMW(Colorcet.BMW), - BMY(Colorcet.BMY), - BROCO(ScientificColourMaps6.BROCO), - BWY(Colorcet.BWY), - - CBSPECTRAL(IDL.CBSPECTRAL), - COLORWHEEL(Colorcet.COLORWHEEL), - COOLWARM(Colorcet.COOLWARM), - CORKO(ScientificColourMaps6.CORKO), - CWR(Colorcet.CWR), - - DIMGRAY(Colorcet.DIMGRAY), - - FIRE(Colorcet.FIRE), - - GLASBEY(Colorcet.GLASBEY), - GLASBEY_DARK(Colorcet.GLASBEY_DARK), - GLASBEY_LIGHT(Colorcet.GLASBEY_LIGHT), - GRAY(Colorcet.GRAY), - GWV(Colorcet.GWV), - - ISOLUM(Colorcet.ISOLUM), - - KB(Colorcet.KB), - KBC(Colorcet.KBC), - KG(Colorcet.KG), - KGY(Colorcet.KGY), - KR(Colorcet.KR), - - OLERON(ScientificColourMaps6.OLERON), - - PARULA(MATLAB.PARULA), - - RAINBOW(Colorcet.RAINBOW), - ROMA(ScientificColourMaps6.ROMA), - ROMAO(ScientificColourMaps6.ROMAO), - - VIKO(ScientificColourMaps6.VIKO), - VIRIDIS(Matplotlib.VIRIDIS); - - private final ColorTable table; - - TYPE(ColorTable table) { - this.table = table; + /** + * + * @param min minimum + * @param minColor color mapped to minimum + * @param max maximum + * @param maxColor color mapped to maximum + * @return divergent color map, based on https://www.kennethmoreland.com/color-maps + */ + public static ColorRamp createDivergent(double min, Color minColor, double max, Color maxColor) { + DivergentColorRamp dcr = new DivergentColorRamp(); + return ImmutableColorRamp.builder() + .min(min) + .max(max) + .colors(dcr.generateColorMap(minColor, maxColor, NUM_COLORS)) + .build(); } - public static Predicate isType(FAMILY type) { - return t -> t.table.getFamily() == type; + /** + * Assign ColorRamp from an array of ints where red is in bits 16-23, green is in bits 8-15, and + * blue is in bits 0-7. + * + * @param min minimum + * @param max maximum + * @param values integer values + * @return color ramp + */ + public static ColorRamp createFromInt(double min, double max, int[] values) { + List colors = new ArrayList<>(); + for (int value : values) colors.add(new Color(value)); + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); } - public List getColors() { - return table.getColors(); + /** + * + * @param min minimum + * @param max maximum + * @param r red + * @param g green + * @param b blue + * @return colormap from arrays of red, green, and blue values. Each color array must have 256 values. + */ + public static ColorRamp createFromRGB(double min, double max, int[] r, int[] g, int[] b) { + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS; i++) colors.add(new Color(r[i], g[i], b[i])); + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); + } + + /** + * @param min minimum + * @param max maximum + * @return a greyscale ramp with the min value black and the max value white. + */ + public static ColorRamp createGreyscale(double min, double max) { + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS; i++) { + colors.add(new Color(i, i, i)); + } + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); + } + + /** + * @param min minimum + * @param max maximum + * @return a circular color ramp running from hue 0 to 360 with saturation and brightness set to + * 1. + */ + public static ColorRamp createHue(double min, double max) { + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS; i++) { + float h = ((float) i) / (NUM_COLORS - 1); + + colors.add(Color.getHSBColor(h, 1.0f, 1.0f)); + } + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); + } + + /** + * + * @param min minimum + * @param minColor color mapped to minimum + * @param max maximum + * @param maxColor color mapped to maximum + * @return color ramp linearly interpolated between minColor and maxColor. + */ + public static ColorRamp createLinear(double min, Color minColor, double max, Color maxColor) { + + final int minRed = minColor.getRed(); + final int redRange = maxColor.getRed() - minColor.getRed(); + final int minGreen = minColor.getGreen(); + final int greenRange = maxColor.getGreen() - minColor.getGreen(); + final int minBlue = minColor.getBlue(); + final int blueRange = maxColor.getBlue() - minColor.getBlue(); + + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS; i++) { + + double frac = ((double) i) / (NUM_COLORS - 1); + + int red = (int) (minRed + frac * redRange); + int green = (int) (minGreen + frac * greenRange); + int blue = (int) (minBlue + frac * blueRange); + + colors.add(new Color(red, green, blue)); + } + + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); + } + + /** + * @param min minimum + * @param max maximum + * @return a linear color ramp running from hue 240 (blue) to hue 0 degrees (red) with saturation + * and brightness set to 1. + */ + public static ColorRamp createLinear(double min, double max) { + List colors = new ArrayList<>(); + for (int i = 0; i < NUM_COLORS; i++) { + float h = (float) (2. / 3. * (1. - i / (NUM_COLORS - 1.))); + float s = 1.0f; + float b = 1.0f; + colors.add(Color.getHSBColor(h, s, b)); + } + return ImmutableColorRamp.builder().min(min).max(max).colors(colors).build(); + } + + public abstract List colors(); + + /** set the lower limit color to black and the upper limit color to white. */ + @Value.Default + public boolean limitColors() { + return false; + } + + public abstract double max(); + + public abstract double min(); + + @Value.Default + public String title() { + return ""; + } + + /** + * @return a ramp with the lower limit color set to black and the upper limit color to white. + */ + public ColorRamp addLimitColors() { + return ImmutableColorRamp.builder().from(this).limitColors(true).build(); + } + + public ColorRamp addTitle(String title) { + return ImmutableColorRamp.builder().from(this).title(title).build(); + } + + /** + * @return a color ramp that is the reverse of this one + */ + public ColorRamp createReverse() { + List newColors = new ArrayList<>(colors()); + Collections.reverse(newColors); + return ImmutableColorRamp.builder().from(this).colors(newColors).build(); + } + + /** + * + * @param value input value + * @return input value mapped to a color. If Double.isFinite(value) is false a transparent Color is + * returned. + */ + public Color getColor(double value) { + + if (Double.isFinite(value)) { + double frac = (value - min()) / (max() - min()); + if (frac < 0) { + if (limitColors()) return Color.BLACK; + frac = 0; + } + if (frac > 1) { + if (limitColors()) return Color.WHITE; + frac = 1; + } + frac *= colors().size(); + return colors().get(Math.min((int) frac, colors().size() - 1)); + } else { + // transparent + return new Color(0, 0, 0, 0); + } + } + + public enum TYPE { + BATLOW(ScientificColourMaps6.BATLOW), + BERLIN(ScientificColourMaps6.BERLIN), + BGY(Colorcet.BGY), + BGYW(Colorcet.BGYW), + BJY(Colorcet.BJY), + BKR(Colorcet.BKR), + BKY(Colorcet.BKY), + BLUES(Colorcet.BLUES), + BMW(Colorcet.BMW), + BMY(Colorcet.BMY), + BROCO(ScientificColourMaps6.BROCO), + BWY(Colorcet.BWY), + + CBSPECTRAL(IDL.CBSPECTRAL), + COLORWHEEL(Colorcet.COLORWHEEL), + COOLWARM(Colorcet.COOLWARM), + CORKO(ScientificColourMaps6.CORKO), + CWR(Colorcet.CWR), + + DIMGRAY(Colorcet.DIMGRAY), + + FIRE(Colorcet.FIRE), + + GLASBEY(Colorcet.GLASBEY), + GLASBEY_DARK(Colorcet.GLASBEY_DARK), + GLASBEY_LIGHT(Colorcet.GLASBEY_LIGHT), + GRAY(Colorcet.GRAY), + GWV(Colorcet.GWV), + + ISOLUM(Colorcet.ISOLUM), + + KB(Colorcet.KB), + KBC(Colorcet.KBC), + KG(Colorcet.KG), + KGY(Colorcet.KGY), + KR(Colorcet.KR), + + OLERON(ScientificColourMaps6.OLERON), + + PARULA(MATLAB.PARULA), + + RAINBOW(Colorcet.RAINBOW), + ROMA(ScientificColourMaps6.ROMA), + ROMAO(ScientificColourMaps6.ROMAO), + + VIKO(ScientificColourMaps6.VIKO), + VIRIDIS(Matplotlib.VIRIDIS); + + private final ColorTable table; + + TYPE(ColorTable table) { + this.table = table; + } + + public static Predicate isType(FAMILY type) { + return t -> t.table.getFamily() == type; + } + + public List getColors() { + return table.getColors(); + } } - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRamp.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRamp.java index 808e250..cee217d 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRamp.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRamp.java @@ -59,13 +59,12 @@ public class DivergentColorRamp { /** * Transfer-matrix for the conversion of RGB to XYZ color space */ - private static final MatrixIJK transferM = new MatrixIJK(0.4124564, 0.2126729, 0.0193339, 0.3575761, 0.7151522, 0.1191920, 0.1804375, 0.0721750, 0.9503041); + private static final MatrixIJK transferM = new MatrixIJK( + 0.4124564, 0.2126729, 0.0193339, 0.3575761, 0.7151522, 0.1191920, 0.1804375, 0.0721750, 0.9503041); private static final MatrixIJK transferI = transferM.createInverse(); - public DivergentColorRamp() { - - } + public DivergentColorRamp() {} public static void test(List colors) { double[] rgb = new double[3]; @@ -79,44 +78,44 @@ public class DivergentColorRamp { double[] rgb2 = app.lin2rgb(lin); System.out.println("RGB->Linear->RGB"); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, rgb[i], lin[i], rgb2[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, rgb[i], lin[i], rgb2[i]); System.out.println("RGB->XYZ->RGB"); double[] xyz = app.rgb2xyz(rgb); rgb2 = app.xyz2rgb(xyz); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, rgb[i], xyz[i], rgb2[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, rgb[i], xyz[i], rgb2[i]); System.out.println("RGB->LAB->RGB"); double[] lab = app.rgb2lab(rgb); rgb2 = app.lab2rgb(lab); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, rgb[i], lab[i], rgb2[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, rgb[i], lab[i], rgb2[i]); System.out.println("RGB->MSH->RGB"); double[] msh = app.rgb2msh(rgb); rgb2 = app.msh2rgb(msh); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, rgb[i], msh[i], rgb2[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, rgb[i], msh[i], rgb2[i]); - double[] color2 = {colors.get(1).getRed(), colors.get(1).getGreen(), colors.get(1).getBlue()}; + double[] color2 = { + colors.get(1).getRed(), colors.get(1).getGreen(), colors.get(1).getBlue() + }; System.out.printf("Color 2: %.0f %.0f %.0f\n", color2[0], color2[1], color2[2]); System.out.println("RGB2->LAB2->RGB2"); double[] lab2 = app.rgb2lab(color2); rgb2 = app.lab2rgb(lab2); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, color2[i], lab2[i], rgb2[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, color2[i], lab2[i], rgb2[i]); - double[] colorN = {colors.get(colors.size() - 1).getRed(), colors.get(colors.size() - 1).getGreen(), colors.get(colors.size() - 1).getBlue()}; + double[] colorN = { + colors.get(colors.size() - 1).getRed(), + colors.get(colors.size() - 1).getGreen(), + colors.get(colors.size() - 1).getBlue() + }; System.out.printf("Color %d: %.0f %.0f %.0f\n", colors.size(), colorN[0], colorN[1], colorN[2]); System.out.println("RGBN->LABN->RGBN"); double[] labN = app.rgb2lab(colorN); rgb2 = app.lab2rgb(labN); - for (int i = 0; i < 3; i++) - System.out.printf("%d %f %f %f\n", i, colorN[i], colorN[i], colorN[i]); + for (int i = 0; i < 3; i++) System.out.printf("%d %f %f %f\n", i, colorN[i], colorN[i], colorN[i]); double x = lab[0] - lab2[0]; double y = lab[1] - lab2[1]; @@ -129,7 +128,6 @@ public class DivergentColorRamp { double endDE = Math.sqrt(x * x + y * y + z * z); System.out.printf("LocalDE %f startDE %f endDE %f\n", localDE, startDE, endDE); - } /** @@ -174,7 +172,7 @@ public class DivergentColorRamp { VectorIJK lin = new VectorIJK(rgb2lin(rgb)); VectorIJK v = transferM.mxv(lin); - return new double[]{v.getI(), v.getJ(), v.getK()}; + return new double[] {v.getI(), v.getJ(), v.getK()}; } /** @@ -229,7 +227,6 @@ public class DivergentColorRamp { xyz[2] = D65.getK() * f.evaluate((lab[0] + 16.) / 116. - (lab[2] / 200.)); return xyz2rgb(xyz); - } /** @@ -238,8 +235,7 @@ public class DivergentColorRamp { private double[] lab2msh(double[] lab) { double sum = 0; - for (int i = 0; i < 3; i++) - sum += lab[i] * lab[i]; + for (int i = 0; i < 3; i++) sum += lab[i] * lab[i]; double[] msh = new double[3]; msh[0] = Math.sqrt(sum); @@ -282,7 +278,8 @@ public class DivergentColorRamp { private double adjustHue(double[] mshSat, double mUnsat) { if (mshSat[0] >= mUnsat) return mshSat[2]; - double hSpin = mshSat[1] * Math.sqrt(mUnsat * mUnsat - mshSat[0] * mshSat[0]) / (mshSat[0] * Math.sin(mshSat[1])); + double hSpin = + mshSat[1] * Math.sqrt(mUnsat * mUnsat - mshSat[0] * mshSat[0]) / (mshSat[0] * Math.sin(mshSat[1])); if (mshSat[2] > -Math.PI / 3) return mshSat[2] + hSpin; @@ -318,7 +315,11 @@ public class DivergentColorRamp { msh2[2] = adjustHue(msh1, msh2[0]); } - double[] mshMid = {(1 - interp) * msh1[0] + interp * msh2[0], (1 - interp) * msh1[1] + interp * msh2[1], (1 - interp) * msh1[2] + interp * msh2[2]}; + double[] mshMid = { + (1 - interp) * msh1[0] + interp * msh2[0], + (1 - interp) * msh1[1] + interp * msh2[1], + (1 - interp) * msh1[2] + interp * msh2[2] + }; return msh2rgb(mshMid); } @@ -340,14 +341,14 @@ public class DivergentColorRamp { int r = (int) Math.max(0, Math.min(255, Math.round(color[0]))); int g = (int) Math.max(0, Math.min(255, Math.round(color[1]))); int b = (int) Math.max(0, Math.min(255, Math.round(color[2]))); - /*- - double[] lin = rgbLinear(color); - double[] lab = rgb2lab(color); - double[] msh = rgb2msh(color); + /*- + double[] lin = rgbLinear(color); + double[] lab = rgb2lab(color); + double[] msh = rgb2msh(color); - System.out.printf("%d %f %3d %3d %3d %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f\n", i, frac, r, - g, b, lin[0] / 100, lin[1] / 100, lin[2] / 100, lab[0], lab[1], lab[2], msh[0], msh[1], msh[2]); - */ + System.out.printf("%d %f %3d %3d %3d %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f %5.3f\n", i, frac, r, + g, b, lin[0] / 100, lin[1] / 100, lin[2] / 100, lab[0], lab[1], lab[2], msh[0], msh[1], msh[2]); + */ colors.add(new Color(r, g, b)); } return colors; @@ -358,8 +359,14 @@ public class DivergentColorRamp { DiscreteDataSet localDE = new DiscreteDataSet("Local ΔE"); DiscreteDataSet startDE = new DiscreteDataSet("Start ΔE"); DiscreteDataSet endDE = new DiscreteDataSet("End ΔE"); - double[] rgb0 = {colors.get(0).getRed(), colors.get(0).getGreen(), colors.get(0).getBlue()}; - double[] rgbN = {colors.get(colors.size() - 1).getRed(), colors.get(colors.size() - 1).getGreen(), colors.get(colors.size() - 1).getBlue()}; + double[] rgb0 = { + colors.get(0).getRed(), colors.get(0).getGreen(), colors.get(0).getBlue() + }; + double[] rgbN = { + colors.get(colors.size() - 1).getRed(), + colors.get(colors.size() - 1).getGreen(), + colors.get(colors.size() - 1).getBlue() + }; double[] lab0 = rgb2lab(rgb0); double[] labN = rgb2lab(rgbN); double frac0 = 0; @@ -400,8 +407,20 @@ public class DivergentColorRamp { startDE.setColor(Color.MAGENTA); endDE.setColor(Color.ORANGE); - PlotConfig config = ImmutablePlotConfig.builder().title(String.format("Min (%d,%d,%d) Max (%d,%d,%d)", colors.get(0).getRed(), colors.get(0).getGreen(), colors.get(0).getBlue(), colors.get(colors.size() - 1).getRed(), colors.get(colors.size() - 1).getGreen(), colors.get(colors.size() - 1).getBlue())).build(); - config = ImmutablePlotConfig.builder().from(config).legendPosition(new Point2D.Double(config.getRightPlotEdge() + 10, config.getTopPlotEdge() + 5)).build(); + PlotConfig config = ImmutablePlotConfig.builder() + .title(String.format( + "Min (%d,%d,%d) Max (%d,%d,%d)", + colors.get(0).getRed(), + colors.get(0).getGreen(), + colors.get(0).getBlue(), + colors.get(colors.size() - 1).getRed(), + colors.get(colors.size() - 1).getGreen(), + colors.get(colors.size() - 1).getBlue())) + .build(); + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.getRightPlotEdge() + 10, config.getTopPlotEdge() + 5)) + .build(); AxisX xAxis = new AxisX(0, colors.size() - 1, "index", "%.0f"); AxisY yAxis = new AxisY(0, 255, "value", "%.0f"); @@ -418,8 +437,14 @@ public class DivergentColorRamp { canvas.addToLegend(endDE.getLegendEntry()); canvas.drawLegend(); - ColorRamp ramp = ImmutableColorRamp.builder().min(0).max(1).colors(colors).build(); - ColorBar cb = ImmutableColorBar.builder().rect(new Rectangle(config.getLeftPlotEdge(), config.getTopPlotEdge() - 40, config.width(), 15)).ramp(ramp).numTicks(5).tickFunction(StringFunctions.fixedFormat("%.2f")).build(); + ColorRamp ramp = + ImmutableColorRamp.builder().min(0).max(1).colors(colors).build(); + ColorBar cb = ImmutableColorBar.builder() + .rect(new Rectangle(config.getLeftPlotEdge(), config.getTopPlotEdge() - 40, config.width(), 15)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build(); canvas.drawColorBar(cb); return canvas.getImage(); @@ -443,6 +468,4 @@ public class DivergentColorRamp { return new Color(r, g, b); } - - } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ColorTable.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ColorTable.java index 595b0dc..7de1f06 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ColorTable.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ColorTable.java @@ -27,19 +27,22 @@ import java.util.List; public abstract class ColorTable { - public enum FAMILY { - CATEGORICAL, CYCLIC, DIVERGENT, LINEAR - } + public enum FAMILY { + CATEGORICAL, + CYCLIC, + DIVERGENT, + LINEAR + } - protected final FAMILY family; + protected final FAMILY family; - protected ColorTable(FAMILY family) { - this.family = family; - } + protected ColorTable(FAMILY family) { + this.family = family; + } - public abstract List getColors(); + public abstract List getColors(); - public FAMILY getFamily() { - return family; - } + public FAMILY getFamily() { + return family; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Colorcet.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Colorcet.java index 519ca92..12bbdcc 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Colorcet.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Colorcet.java @@ -52,801 +52,690 @@ import java.util.List; */ public class Colorcet extends ColorTable { - private static final int[] bgy = { - 3196, 3198, 3456, 3458, 3715, 3717, 3975, 3977, 4235, 4236, 4494, 4752, 4754, 5011, 5013, 5271, - 5528, 5530, 5788, 6045, 6047, 6304, 6562, 6819, 6821, 7078, 7336, 7593, 7851, 7852, 8109, 8367, - 8624, 8881, 9138, 9395, 9652, 9909, 10166, 10423, 10680, 10936, 11193, 11450, 11962, 12218, - 12475, 12731, 13243, 13499, 13755, 14011, 14267, 14779, 15035, 15291, 15547, 15803, 16314, - 16570, 16826, 17081, 17337, 17848, 18104, 18359, 18614, 18870, 19125, 19636, 19891, 20146, - 20402, 20657, 21168, 21422, 21677, 21932, 22187, 22442, 22953, 23208, 23463, 23717, 23972, - 24483, 24738, 24993, 25248, 25502, 25757, 26012, 26523, 26778, 27032, 27287, 27542, 27797, - 28051, 28562, 28817, 29071, 29326, 29581, 29835, 30090, 30345, 30855, 31110, 31364, 31619, - 31873, 32128, 32382, 32637, 32891, 33401, 33655, 33910, 165236, 427634, 624496, 886894, 1083756, - 1215082, 1411944, 1543270, 1674595, 1805921, 1937503, 2068828, 2134618, 2265944, 2331733, - 2463058, 2528848, 2660173, 2725962, 2791752, 2857541, 2923331, 2923584, 2989374, 2989627, - 3055417, 3055671, 3121461, 3121714, 3121968, 3188014, 3188268, 3188522, 3188776, 3189030, - 3254821, 3255075, 3255329, 3255584, 3255839, 3256093, 3256348, 3256603, 3256858, 3322650, - 3322906, 3323161, 3323417, 3323673, 3323929, 3324185, 3324441, 3324697, 3390489, 3390745, - 3391001, 3391257, 3391513, 3391769, 3392025, 3392281, 3458329, 3458585, 3458841, 3459097, - 3459353, 3459609, 3525401, 3525657, 3525913, 3526169, 3526425, 3526681, 3592473, 3592729, - 3592985, 3593241, 3659033, 3724825, 3921689, 4053017, 4249881, 4512281, 4708889, 4971289, - 5233690, 5496090, 5758490, 5955098, 6217498, 6479898, 6742042, 7004442, 7266842, 7528986, - 7791386, 8053531, 8315931, 8578331, 8840475, 9102875, 9365019, 9561883, 9824027, 10086428, - 10348572, 10610972, 10807580, 11069980, 11332124, 11594268, 11791133, 12053277, 12315677, - 12512285, 12774429, 13036830, 13233438, 13495582, 13757982, 13954590, 14216735, 14478879, - 14675743, 14937887, 15200032, 15396640, 15659040, 15921184, 16117793, 16379937, 16576545, - 16773153, 16773154, 16773410, 16773410, 16773411 - }; - public static final Colorcet BGY = new Colorcet(bgy, FAMILY.LINEAR); + private static final int[] bgy = { + 3196, 3198, 3456, 3458, 3715, 3717, 3975, 3977, 4235, 4236, 4494, 4752, 4754, 5011, 5013, 5271, 5528, 5530, + 5788, 6045, 6047, 6304, 6562, 6819, 6821, 7078, 7336, 7593, 7851, 7852, 8109, 8367, 8624, 8881, 9138, 9395, + 9652, 9909, 10166, 10423, 10680, 10936, 11193, 11450, 11962, 12218, 12475, 12731, 13243, 13499, 13755, 14011, + 14267, 14779, 15035, 15291, 15547, 15803, 16314, 16570, 16826, 17081, 17337, 17848, 18104, 18359, 18614, 18870, + 19125, 19636, 19891, 20146, 20402, 20657, 21168, 21422, 21677, 21932, 22187, 22442, 22953, 23208, 23463, 23717, + 23972, 24483, 24738, 24993, 25248, 25502, 25757, 26012, 26523, 26778, 27032, 27287, 27542, 27797, 28051, 28562, + 28817, 29071, 29326, 29581, 29835, 30090, 30345, 30855, 31110, 31364, 31619, 31873, 32128, 32382, 32637, 32891, + 33401, 33655, 33910, 165236, 427634, 624496, 886894, 1083756, 1215082, 1411944, 1543270, 1674595, 1805921, + 1937503, 2068828, 2134618, 2265944, 2331733, 2463058, 2528848, 2660173, 2725962, 2791752, 2857541, 2923331, + 2923584, 2989374, 2989627, 3055417, 3055671, 3121461, 3121714, 3121968, 3188014, 3188268, 3188522, 3188776, + 3189030, 3254821, 3255075, 3255329, 3255584, 3255839, 3256093, 3256348, 3256603, 3256858, 3322650, 3322906, + 3323161, 3323417, 3323673, 3323929, 3324185, 3324441, 3324697, 3390489, 3390745, 3391001, 3391257, 3391513, + 3391769, 3392025, 3392281, 3458329, 3458585, 3458841, 3459097, 3459353, 3459609, 3525401, 3525657, 3525913, + 3526169, 3526425, 3526681, 3592473, 3592729, 3592985, 3593241, 3659033, 3724825, 3921689, 4053017, 4249881, + 4512281, 4708889, 4971289, 5233690, 5496090, 5758490, 5955098, 6217498, 6479898, 6742042, 7004442, 7266842, + 7528986, 7791386, 8053531, 8315931, 8578331, 8840475, 9102875, 9365019, 9561883, 9824027, 10086428, 10348572, + 10610972, 10807580, 11069980, 11332124, 11594268, 11791133, 12053277, 12315677, 12512285, 12774429, 13036830, + 13233438, 13495582, 13757982, 13954590, 14216735, 14478879, 14675743, 14937887, 15200032, 15396640, 15659040, + 15921184, 16117793, 16379937, 16576545, 16773153, 16773154, 16773410, 16773410, 16773411 + }; + public static final Colorcet BGY = new Colorcet(bgy, FAMILY.LINEAR); - private static final int[] bgyw = { - 1704069, 1704071, 1704073, 1704075, 1704077, 1704334, 1704592, 1704594, 1704852, 1705109, - 1705367, 1705625, 1705883, 1706140, 1706398, 1706656, 1706913, 1707171, 1707429, 1707686, - 1707944, 1708202, 1708459, 1708717, 1708974, 1709232, 1709490, 1709747, 1710005, 1710262, - 1710520, 1710777, 1711034, 1711292, 1711549, 1711807, 1712064, 1712577, 1712835, 1778628, - 1778885, 1779143, 1779400, 1779657, 1779914, 1780171, 1780684, 1780941, 1846734, 1846991, - 1847248, 1847761, 1848018, 1913811, 1914067, 1914580, 1914837, 1915093, 1981142, 1981398, - 1981654, 1982167, 2047959, 2048471, 2048727, 2049239, 2115030, 2115542, 2115797, 2116308, - 2116819, 2117330, 2117585, 2118095, 2118604, 2119370, 2054343, 2054852, 2055361, 2055870, - 2121915, 2122168, 2188213, 2188722, 2254511, 2320556, 2386601, 2452390, 2518436, 2584225, - 2650270, 2716059, 2781849, 2847894, 2913683, 2979473, 3045518, 3111307, 3177096, 3242886, - 3308931, 3374721, 3440510, 3440763, 3506809, 3572598, 3638387, 3704177, 3769966, 3770219, - 3836265, 3902054, 3967843, 3968097, 4033886, 4099675, 4165465, 4165719, 4231508, 4297298, - 4363088, 4428878, 4494924, 4560714, 4626504, 4692038, 4757828, 4823618, 4889408, 5020734, - 5086525, 5152315, 5218105, 5349432, 5415222, 5546549, 5612339, 5743666, 5809456, 5940527, - 6006317, 6137644, 6203435, 6334761, 6466088, 6531879, 6662950, 6794276, 6925603, 7056930, - 7122721, 7253792, 7385119, 7516446, 7647773, 7778844, 7910171, 8041498, 8172825, 8303897, - 8435224, 8566552, 8697879, 8828951, 8960278, 9091606, 9222678, 9354006, 9485333, 9682198, - 9813270, 9944598, 10075926, 10206998, 10338327, 10534935, 10666264, 10797592, 10928665, - 11059993, 11191322, 11322395, 11453723, 11585052, 11716125, 11847454, 11978782, 12109855, - 12241184, 12372513, 12503586, 12634915, 12766244, 12897317, 13028646, 13094439, 13225512, - 13356841, 13488171, 13553964, 13685037, 13816366, 13947696, 14013233, 14144563, 14210356, - 14341686, 14472759, 14538553, 14669883, 14735676, 14801470, 14932544, 14998338, 15064132, - 15195462, 15261256, 15327050, 15392588, 15458383, 15524177, 15524436, 15590230, 15656025, - 15656284, 15722079, 15722339, 15722854, 15788650, 15788909, 15789169, 15854965, 15855225, - 15921022, 15921026, 15986823, 15987083, 16052880, 16118677, 16118938, 16184735, 16250277, - 16250538, 16316336, 16382134, 16382140, 16447938, 16513736, 16513742, 16579541, 16579803, - 16645346, 16645609, 16711152, 16711415, 16711679 - }; - public static final Colorcet BGYW = new Colorcet(bgyw, FAMILY.LINEAR); + private static final int[] bgyw = { + 1704069, 1704071, 1704073, 1704075, 1704077, 1704334, 1704592, 1704594, 1704852, 1705109, 1705367, 1705625, + 1705883, 1706140, 1706398, 1706656, 1706913, 1707171, 1707429, 1707686, 1707944, 1708202, 1708459, 1708717, + 1708974, 1709232, 1709490, 1709747, 1710005, 1710262, 1710520, 1710777, 1711034, 1711292, 1711549, 1711807, + 1712064, 1712577, 1712835, 1778628, 1778885, 1779143, 1779400, 1779657, 1779914, 1780171, 1780684, 1780941, + 1846734, 1846991, 1847248, 1847761, 1848018, 1913811, 1914067, 1914580, 1914837, 1915093, 1981142, 1981398, + 1981654, 1982167, 2047959, 2048471, 2048727, 2049239, 2115030, 2115542, 2115797, 2116308, 2116819, 2117330, + 2117585, 2118095, 2118604, 2119370, 2054343, 2054852, 2055361, 2055870, 2121915, 2122168, 2188213, 2188722, + 2254511, 2320556, 2386601, 2452390, 2518436, 2584225, 2650270, 2716059, 2781849, 2847894, 2913683, 2979473, + 3045518, 3111307, 3177096, 3242886, 3308931, 3374721, 3440510, 3440763, 3506809, 3572598, 3638387, 3704177, + 3769966, 3770219, 3836265, 3902054, 3967843, 3968097, 4033886, 4099675, 4165465, 4165719, 4231508, 4297298, + 4363088, 4428878, 4494924, 4560714, 4626504, 4692038, 4757828, 4823618, 4889408, 5020734, 5086525, 5152315, + 5218105, 5349432, 5415222, 5546549, 5612339, 5743666, 5809456, 5940527, 6006317, 6137644, 6203435, 6334761, + 6466088, 6531879, 6662950, 6794276, 6925603, 7056930, 7122721, 7253792, 7385119, 7516446, 7647773, 7778844, + 7910171, 8041498, 8172825, 8303897, 8435224, 8566552, 8697879, 8828951, 8960278, 9091606, 9222678, 9354006, + 9485333, 9682198, 9813270, 9944598, 10075926, 10206998, 10338327, 10534935, 10666264, 10797592, 10928665, + 11059993, 11191322, 11322395, 11453723, 11585052, 11716125, 11847454, 11978782, 12109855, 12241184, 12372513, + 12503586, 12634915, 12766244, 12897317, 13028646, 13094439, 13225512, 13356841, 13488171, 13553964, 13685037, + 13816366, 13947696, 14013233, 14144563, 14210356, 14341686, 14472759, 14538553, 14669883, 14735676, 14801470, + 14932544, 14998338, 15064132, 15195462, 15261256, 15327050, 15392588, 15458383, 15524177, 15524436, 15590230, + 15656025, 15656284, 15722079, 15722339, 15722854, 15788650, 15788909, 15789169, 15854965, 15855225, 15921022, + 15921026, 15986823, 15987083, 16052880, 16118677, 16118938, 16184735, 16250277, 16250538, 16316336, 16382134, + 16382140, 16447938, 16513736, 16513742, 16579541, 16579803, 16645346, 16645609, 16711152, 16711415, 16711679 + }; + public static final Colorcet BGYW = new Colorcet(bgyw, FAMILY.LINEAR); - private static final int[] bjy = { - 1257921, 1585857, 1848000, 2044864, 2241727, 2438335, 2569663, 2766526, 2897854, 3028926, - 3160253, 3291581, 3422909, 3553980, 3685308, 3751100, 3882171, 4013499, 4079291, 4210618, - 4276154, 4341945, 4473273, 4539065, 4604600, 4735928, 4801720, 4867255, 4933047, 5064375, - 5130166, 5195702, 5261494, 5327285, 5393077, 5458612, 5524404, 5590196, 5655987, 5721523, - 5787315, 5853106, 5918898, 5984434, 6050225, 6116017, 6181552, 6247344, 6313136, 6378927, - 6378927, 6444719, 6510510, 6576302, 6641837, 6707629, 6707885, 6773676, 6839212, 6905004, - 6905259, 6971051, 7036587, 7102378, 7102634, 7168425, 7233961, 7299753, 7300008, 7365800, - 7431335, 7431591, 7497383, 7563174, 7563174, 7628966, 7694757, 7695013, 7760548, 7826340, - 7826596, 7892387, 7892387, 7958178, 8023970, 8024226, 8089761, 8090017, 8155808, 8221600, - 8221600, 8287391, 8287647, 8353438, 8418974, 8419230, 8485021, 8485277, 8550812, 8551068, - 8616860, 8617115, 8682651, 8682906, 8748698, 8748954, 8814745, 8814745, 8880536, 8880792, - 8946584, 8946583, 9012375, 9012630, 9078422, 9078421, 9144213, 9144469, 9210260, 9210516, - 9276051, 9276307, 9342098, 9342354, 9342354, 9408145, 9408401, 9474192, 9539728, 9605519, - 9671311, 9736846, 9802637, 9868173, 9933964, 9999756, 10065291, 10131082, 10196618, 10262409, - 10328200, 10393736, 10459527, 10525319, 10590854, 10656645, 10656645, 10722436, 10788227, - 10853763, 10919554, 10985346, 11050881, 11116672, 11182208, 11247999, 11313790, 11379326, - 11445117, 11445372, 11510908, 11576699, 11642490, 11708026, 11773817, 11839352, 11905144, - 11970935, 11970934, 12036726, 12102517, 12168052, 12233843, 12299635, 12365170, 12430961, - 12430961, 12496752, 12562543, 12628078, 12693870, 12759661, 12825196, 12825451, 12891243, - 12956778, 13022569, 13088360, 13153896, 13154151, 13219942, 13285477, 13351269, 13417060, - 13417059, 13482850, 13548641, 13614176, 13679968, 13745759, 13745758, 13811549, 13877340, - 13942875, 14008666, 14008922, 14074457, 14140248, 14206039, 14271574, 14271829, 14337620, - 14403155, 14468946, 14534737, 14534736, 14600527, 14666318, 14731853, 14797644, 14797899, - 14863434, 14929225, 14995016, 14995015, 15060806, 15126596, 15192131, 15257922, 15258177, - 15323712, 15389502, 15455293, 15455292, 15521082, 15586873, 15652407, 15718198, 15718452, - 15783987, 15849777, 15915568, 15915566, 15981356, 16047146, 16112936, 16112934, 16178724, - 16244514, 16310047, 16310301, 16376090, 16441622, 16507411, 16507662, 16573193 - }; - public static final Colorcet BJY = new Colorcet(bjy, FAMILY.DIVERGENT); + private static final int[] bjy = { + 1257921, 1585857, 1848000, 2044864, 2241727, 2438335, 2569663, 2766526, 2897854, 3028926, 3160253, 3291581, + 3422909, 3553980, 3685308, 3751100, 3882171, 4013499, 4079291, 4210618, 4276154, 4341945, 4473273, 4539065, + 4604600, 4735928, 4801720, 4867255, 4933047, 5064375, 5130166, 5195702, 5261494, 5327285, 5393077, 5458612, + 5524404, 5590196, 5655987, 5721523, 5787315, 5853106, 5918898, 5984434, 6050225, 6116017, 6181552, 6247344, + 6313136, 6378927, 6378927, 6444719, 6510510, 6576302, 6641837, 6707629, 6707885, 6773676, 6839212, 6905004, + 6905259, 6971051, 7036587, 7102378, 7102634, 7168425, 7233961, 7299753, 7300008, 7365800, 7431335, 7431591, + 7497383, 7563174, 7563174, 7628966, 7694757, 7695013, 7760548, 7826340, 7826596, 7892387, 7892387, 7958178, + 8023970, 8024226, 8089761, 8090017, 8155808, 8221600, 8221600, 8287391, 8287647, 8353438, 8418974, 8419230, + 8485021, 8485277, 8550812, 8551068, 8616860, 8617115, 8682651, 8682906, 8748698, 8748954, 8814745, 8814745, + 8880536, 8880792, 8946584, 8946583, 9012375, 9012630, 9078422, 9078421, 9144213, 9144469, 9210260, 9210516, + 9276051, 9276307, 9342098, 9342354, 9342354, 9408145, 9408401, 9474192, 9539728, 9605519, 9671311, 9736846, + 9802637, 9868173, 9933964, 9999756, 10065291, 10131082, 10196618, 10262409, 10328200, 10393736, 10459527, + 10525319, 10590854, 10656645, 10656645, 10722436, 10788227, 10853763, 10919554, 10985346, 11050881, 11116672, + 11182208, 11247999, 11313790, 11379326, 11445117, 11445372, 11510908, 11576699, 11642490, 11708026, 11773817, + 11839352, 11905144, 11970935, 11970934, 12036726, 12102517, 12168052, 12233843, 12299635, 12365170, 12430961, + 12430961, 12496752, 12562543, 12628078, 12693870, 12759661, 12825196, 12825451, 12891243, 12956778, 13022569, + 13088360, 13153896, 13154151, 13219942, 13285477, 13351269, 13417060, 13417059, 13482850, 13548641, 13614176, + 13679968, 13745759, 13745758, 13811549, 13877340, 13942875, 14008666, 14008922, 14074457, 14140248, 14206039, + 14271574, 14271829, 14337620, 14403155, 14468946, 14534737, 14534736, 14600527, 14666318, 14731853, 14797644, + 14797899, 14863434, 14929225, 14995016, 14995015, 15060806, 15126596, 15192131, 15257922, 15258177, 15323712, + 15389502, 15455293, 15455292, 15521082, 15586873, 15652407, 15718198, 15718452, 15783987, 15849777, 15915568, + 15915566, 15981356, 16047146, 16112936, 16112934, 16178724, 16244514, 16310047, 16310301, 16376090, 16441622, + 16507411, 16507662, 16573193 + }; + public static final Colorcet BJY = new Colorcet(bjy, FAMILY.DIVERGENT); - private static final int[] bkr = { - 1606138, 1736952, 1802230, 1867508, 1998322, 2063600, 2129134, 2194412, 2259690, 2324968, - 2390246, 2389988, 2455266, 2520544, 2586078, 2585821, 2651099, 2650841, 2716119, 2781397, - 2781139, 2846673, 2846415, 2846157, 2911435, 2911177, 2976455, 2976198, 2976196, 3041474, - 3041216, 3040958, 3106236, 3105978, 3105720, 3105719, 3170997, 3170739, 3170481, 3170223, - 3169965, 3169963, 3235242, 3234984, 3234726, 3234468, 3234210, 3234208, 3233951, 3233693, - 3233435, 3233177, 3232919, 3232917, 3232660, 3232402, 3232144, 3231886, 3231884, 3231627, - 3231369, 3231111, 3230853, 3230852, 3230594, 3230336, 3164542, 3164285, 3164283, 3164025, - 3163767, 3163510, 3163252, 3097714, 3097456, 3097199, 3096941, 3096683, 3031146, 3030888, - 3030630, 3030373, 3030371, 2964577, 2964320, 2964062, 2963804, 2898267, 2898009, 2897751, - 2831958, 2831956, 2831698, 2765905, 2765647, 2765646, 2699852, 2699594, 2699337, 2633799, - 2633542, 2633284, 2567746, 2567489, 2567231, 2501438, 2501436, 2435643, 2435385, 2369591, - 2369590, 2369332, 2303539, 2303537, 2237744, 2237487, 2237485, 2171692, 2171434, 2171433, - 2171176, 2105638, 2105381, 2105380, 2170659, 2170658, 2170657, 2170657, 2236192, 2301728, - 2301727, 2367263, 2432799, 2498334, 2563870, 2629662, 2760734, 2826271, 2892063, 3023135, - 3088927, 3154463, 3285536, 3351328, 3482400, 3548192, 3679265, 3745057, 3876129, 3941922, - 4007458, 4138530, 4204322, 4335395, 4401187, 4532259, 4598052, 4729124, 4794916, 4925988, - 4991781, 5122853, 5188389, 5319718, 5385254, 5516582, 5582119, 5713447, 5778983, 5910055, - 5975848, 6106920, 6172712, 6303785, 6369577, 6500649, 6566185, 6697514, 6828586, 6894378, - 7025451, 7091243, 7222315, 7287852, 7419180, 7484716, 7616044, 7681581, 7812653, 7943981, - 8009518, 8140846, 8206382, 8337454, 8403247, 8534319, 8665647, 8731184, 8862256, 8928048, - 9059121, 9190449, 9255985, 9387057, 9452850, 9583922, 9715250, 9780787, 9911859, 9977651, - 10108724, 10239796, 10305588, 10436660, 10567989, 10633525, 10764597, 10830390, 10961462, - 11092534, 11158327, 11289399, 11420727, 11486263, 11617336, 11748664, 11814200, 11945273, - 12076601, 12142137, 12273465, 12404538, 12470074, 12601402, 12732475, 12798011, 12929339, - 13060412, 13125948, 13257276, 13388348, 13519421, 13585213, 13716285, 13847358, 13913150, - 14044222, 14175551, 14306623, 14372159, 14503487, 14634560, 14700096, 14831424, 14962497, - 15093569 - }; - public static final Colorcet BKR = new Colorcet(bkr, FAMILY.DIVERGENT); + private static final int[] bkr = { + 1606138, 1736952, 1802230, 1867508, 1998322, 2063600, 2129134, 2194412, 2259690, 2324968, 2390246, 2389988, + 2455266, 2520544, 2586078, 2585821, 2651099, 2650841, 2716119, 2781397, 2781139, 2846673, 2846415, 2846157, + 2911435, 2911177, 2976455, 2976198, 2976196, 3041474, 3041216, 3040958, 3106236, 3105978, 3105720, 3105719, + 3170997, 3170739, 3170481, 3170223, 3169965, 3169963, 3235242, 3234984, 3234726, 3234468, 3234210, 3234208, + 3233951, 3233693, 3233435, 3233177, 3232919, 3232917, 3232660, 3232402, 3232144, 3231886, 3231884, 3231627, + 3231369, 3231111, 3230853, 3230852, 3230594, 3230336, 3164542, 3164285, 3164283, 3164025, 3163767, 3163510, + 3163252, 3097714, 3097456, 3097199, 3096941, 3096683, 3031146, 3030888, 3030630, 3030373, 3030371, 2964577, + 2964320, 2964062, 2963804, 2898267, 2898009, 2897751, 2831958, 2831956, 2831698, 2765905, 2765647, 2765646, + 2699852, 2699594, 2699337, 2633799, 2633542, 2633284, 2567746, 2567489, 2567231, 2501438, 2501436, 2435643, + 2435385, 2369591, 2369590, 2369332, 2303539, 2303537, 2237744, 2237487, 2237485, 2171692, 2171434, 2171433, + 2171176, 2105638, 2105381, 2105380, 2170659, 2170658, 2170657, 2170657, 2236192, 2301728, 2301727, 2367263, + 2432799, 2498334, 2563870, 2629662, 2760734, 2826271, 2892063, 3023135, 3088927, 3154463, 3285536, 3351328, + 3482400, 3548192, 3679265, 3745057, 3876129, 3941922, 4007458, 4138530, 4204322, 4335395, 4401187, 4532259, + 4598052, 4729124, 4794916, 4925988, 4991781, 5122853, 5188389, 5319718, 5385254, 5516582, 5582119, 5713447, + 5778983, 5910055, 5975848, 6106920, 6172712, 6303785, 6369577, 6500649, 6566185, 6697514, 6828586, 6894378, + 7025451, 7091243, 7222315, 7287852, 7419180, 7484716, 7616044, 7681581, 7812653, 7943981, 8009518, 8140846, + 8206382, 8337454, 8403247, 8534319, 8665647, 8731184, 8862256, 8928048, 9059121, 9190449, 9255985, 9387057, + 9452850, 9583922, 9715250, 9780787, 9911859, 9977651, 10108724, 10239796, 10305588, 10436660, 10567989, + 10633525, 10764597, 10830390, 10961462, 11092534, 11158327, 11289399, 11420727, 11486263, 11617336, 11748664, + 11814200, 11945273, 12076601, 12142137, 12273465, 12404538, 12470074, 12601402, 12732475, 12798011, 12929339, + 13060412, 13125948, 13257276, 13388348, 13519421, 13585213, 13716285, 13847358, 13913150, 14044222, 14175551, + 14306623, 14372159, 14503487, 14634560, 14700096, 14831424, 14962497, 15093569 + }; + public static final Colorcet BKR = new Colorcet(bkr, FAMILY.DIVERGENT); - private static final int[] bky = { - 955386, 1086200, 1217014, 1347828, 1478642, 1609456, 1674734, 1805548, 1870826, 1936104, - 2001382, 2066660, 2131938, 2197216, 2262494, 2327772, 2393050, 2392792, 2458071, 2523349, - 2523091, 2588369, 2653647, 2653133, 2718411, 2718153, 2783431, 2783173, 2782915, 2848194, - 2847936, 2913214, 2912956, 2912698, 2977976, 2977718, 2977460, 2977203, 3042481, 3042223, - 3041965, 3041707, 3106985, 3106727, 3106470, 3106468, 3106210, 3105952, 3105694, 3105436, - 3170715, 3170457, 3170199, 3169941, 3169683, 3169426, 3169168, 3168910, 3168652, 3168395, - 3168137, 3167879, 3167621, 3167363, 3167106, 3101312, 3101054, 3100796, 3100539, 3100281, - 3100279, 3100021, 3099764, 3099506, 3033712, 3033455, 3033197, 3032939, 3032681, 2966888, - 2966630, 2966372, 2966115, 2966113, 2900319, 2900062, 2899804, 2899546, 2833753, 2833495, - 2833237, 2832980, 2767186, 2767185, 2766927, 2701133, 2700876, 2700618, 2634825, 2634567, - 2634309, 2568772, 2568514, 2568257, 2502463, 2502206, 2436412, 2436154, 2436153, 2370359, - 2370102, 2304308, 2304051, 2303793, 2238256, 2237998, 2172205, 2171948, 2171946, 2106153, - 2105896, 2105894, 2105637, 2105636, 2105635, 2105378, 2105377, 2105376, 2170912, 2170911, - 2170911, 2236446, 2302238, 2302237, 2367773, 2433565, 2499357, 2564893, 2630685, 2696477, - 2762013, 2827805, 2893597, 2959133, 3024925, 3090718, 3156510, 3287838, 3353374, 3419166, - 3484958, 3550750, 3616286, 3682078, 3747871, 3813663, 3944991, 4010783, 4076319, 4142111, - 4207903, 4273695, 4339487, 4405023, 4536352, 4602144, 4667936, 4733728, 4799520, 4865056, - 4930848, 4996640, 5127968, 5193760, 5259552, 5325088, 5390880, 5456672, 5588000, 5653792, - 5719585, 5785377, 5850913, 5916705, 5982497, 6113825, 6179617, 6245409, 6311201, 6376993, - 6442529, 6573857, 6639649, 6705441, 6771233, 6837025, 6968353, 7034145, 7099681, 7165473, - 7231265, 7362593, 7428385, 7494177, 7559969, 7625761, 7757089, 7822881, 7888417, 7954209, - 8020001, 8151328, 8217120, 8282912, 8348704, 8414496, 8545824, 8611616, 8677408, 8742944, - 8874272, 8940064, 9005856, 9071648, 9137439, 9268767, 9334559, 9400351, 9466143, 9597471, - 9663263, 9729055, 9794846, 9925918, 9991710, 10057502, 10123294, 10254622, 10320413, 10386205, - 10451997, 10583325, 10649117, 10714909, 10846236, 10912028, 10977820, 11043612, 11174939, - 11240731, 11306523, 11372315, 11503642, 11569434, 11634970, 11766297 - }; - public static final Colorcet BKY = new Colorcet(bky, FAMILY.DIVERGENT); + private static final int[] bky = { + 955386, 1086200, 1217014, 1347828, 1478642, 1609456, 1674734, 1805548, 1870826, 1936104, 2001382, 2066660, + 2131938, 2197216, 2262494, 2327772, 2393050, 2392792, 2458071, 2523349, 2523091, 2588369, 2653647, 2653133, + 2718411, 2718153, 2783431, 2783173, 2782915, 2848194, 2847936, 2913214, 2912956, 2912698, 2977976, 2977718, + 2977460, 2977203, 3042481, 3042223, 3041965, 3041707, 3106985, 3106727, 3106470, 3106468, 3106210, 3105952, + 3105694, 3105436, 3170715, 3170457, 3170199, 3169941, 3169683, 3169426, 3169168, 3168910, 3168652, 3168395, + 3168137, 3167879, 3167621, 3167363, 3167106, 3101312, 3101054, 3100796, 3100539, 3100281, 3100279, 3100021, + 3099764, 3099506, 3033712, 3033455, 3033197, 3032939, 3032681, 2966888, 2966630, 2966372, 2966115, 2966113, + 2900319, 2900062, 2899804, 2899546, 2833753, 2833495, 2833237, 2832980, 2767186, 2767185, 2766927, 2701133, + 2700876, 2700618, 2634825, 2634567, 2634309, 2568772, 2568514, 2568257, 2502463, 2502206, 2436412, 2436154, + 2436153, 2370359, 2370102, 2304308, 2304051, 2303793, 2238256, 2237998, 2172205, 2171948, 2171946, 2106153, + 2105896, 2105894, 2105637, 2105636, 2105635, 2105378, 2105377, 2105376, 2170912, 2170911, 2170911, 2236446, + 2302238, 2302237, 2367773, 2433565, 2499357, 2564893, 2630685, 2696477, 2762013, 2827805, 2893597, 2959133, + 3024925, 3090718, 3156510, 3287838, 3353374, 3419166, 3484958, 3550750, 3616286, 3682078, 3747871, 3813663, + 3944991, 4010783, 4076319, 4142111, 4207903, 4273695, 4339487, 4405023, 4536352, 4602144, 4667936, 4733728, + 4799520, 4865056, 4930848, 4996640, 5127968, 5193760, 5259552, 5325088, 5390880, 5456672, 5588000, 5653792, + 5719585, 5785377, 5850913, 5916705, 5982497, 6113825, 6179617, 6245409, 6311201, 6376993, 6442529, 6573857, + 6639649, 6705441, 6771233, 6837025, 6968353, 7034145, 7099681, 7165473, 7231265, 7362593, 7428385, 7494177, + 7559969, 7625761, 7757089, 7822881, 7888417, 7954209, 8020001, 8151328, 8217120, 8282912, 8348704, 8414496, + 8545824, 8611616, 8677408, 8742944, 8874272, 8940064, 9005856, 9071648, 9137439, 9268767, 9334559, 9400351, + 9466143, 9597471, 9663263, 9729055, 9794846, 9925918, 9991710, 10057502, 10123294, 10254622, 10320413, 10386205, + 10451997, 10583325, 10649117, 10714909, 10846236, 10912028, 10977820, 11043612, 11174939, 11240731, 11306523, + 11372315, 11503642, 11569434, 11634970, 11766297 + }; + public static final Colorcet BKY = new Colorcet(bky, FAMILY.DIVERGENT); - private static final int[] blues = { - 15790320, 15724784, 15724528, 15658992, 15593200, 15527664, 15461872, 15461872, 15396080, - 15330544, 15264751, 15264751, 15198959, 15133423, 15067631, 15067631, 15002095, 14936303, - 14870767, 14870511, 14804975, 14739183, 14673647, 14673391, 14607854, 14542062, 14476526, - 14410734, 14410734, 14344942, 14279406, 14213614, 14213614, 14147822, 14082286, 14016750, - 14016493, 13950957, 13885165, 13819629, 13819373, 13753837, 13688045, 13622509, 13556717, - 13556717, 13490925, 13425388, 13359596, 13359596, 13294060, 13228268, 13162732, 13162476, - 13096940, 13031148, 12965611, 12965355, 12899819, 12834027, 12768491, 12702699, 12702699, - 12636907, 12571371, 12505834, 12505578, 12440042, 12374250, 12308714, 12308458, 12242922, - 12177130, 12111593, 12111337, 12045801, 11980009, 11914473, 11914217, 11848680, 11783144, - 11717352, 11717352, 11651560, 11586024, 11520232, 11520231, 11454439, 11388903, 11323111, - 11323111, 11257318, 11191782, 11126246, 11125990, 11060454, 10994661, 10929125, 10928869, - 10863333, 10797541, 10797540, 10731748, 10666212, 10600420, 10600419, 10534627, 10469091, - 10469091, 10403298, 10337762, 10337506, 10271969, 10206177, 10206177, 10140384, 10140384, - 10074592, 10009055, 10008799, 9943263, 9943006, 9877470, 9811934, 9811677, 9746141, 9745884, - 9680348, 9614556, 9614555, 9548763, 9548762, 9482970, 9417434, 9417177, 9351641, 9351385, - 9285848, 9285592, 9220055, 9154519, 9154262, 9088726, 9088470, 9022933, 9022677, 8957140, - 8956884, 8891348, 8891091, 8825555, 8825298, 8759762, 8693969, 8693969, 8628176, 8628176, - 8562639, 8562383, 8496847, 8496590, 8431054, 8430797, 8365261, 8365004, 8299468, 8299211, - 8233675, 8233418, 8167882, 8167625, 8167625, 8102088, 8101832, 8036295, 7970503, 7970503, - 7904710, 7904710, 7838917, 7838917, 7773124, 7773124, 7707332, 7641795, 7641795, 7576003, - 7576002, 7510210, 7444674, 7444417, 7378881, 7378625, 7313088, 7247296, 7247296, 7181759, - 7115967, 7115967, 7050174, 6984638, 6984382, 6918845, 6853053, 6787517, 6787517, 6721724, - 6656188, 6655932, 6590395, 6524603, 6459067, 6458811, 6393274, 6327738, 6261946, 6261946, - 6196153, 6130617, 6064825, 5999289, 5999288, 5933496, 5867960, 5802168, 5736631, 5736375, - 5670839, 5605047, 5539511, 5473974, 5408182, 5342646, 5276854, 5276853, 5211061, 5145525, - 5079989, 5014197, 4948660, 4882868, 4817332, 4751540, 4686004, 4620467, 4554675, 4489139, - 4423347, 4357811, 4292274, 4226482, 4160946, 4095154, 3964082, 3898290, 3832753 - }; - public static final Colorcet BLUES = new Colorcet(blues, FAMILY.LINEAR); + private static final int[] blues = { + 15790320, 15724784, 15724528, 15658992, 15593200, 15527664, 15461872, 15461872, 15396080, 15330544, 15264751, + 15264751, 15198959, 15133423, 15067631, 15067631, 15002095, 14936303, 14870767, 14870511, 14804975, 14739183, + 14673647, 14673391, 14607854, 14542062, 14476526, 14410734, 14410734, 14344942, 14279406, 14213614, 14213614, + 14147822, 14082286, 14016750, 14016493, 13950957, 13885165, 13819629, 13819373, 13753837, 13688045, 13622509, + 13556717, 13556717, 13490925, 13425388, 13359596, 13359596, 13294060, 13228268, 13162732, 13162476, 13096940, + 13031148, 12965611, 12965355, 12899819, 12834027, 12768491, 12702699, 12702699, 12636907, 12571371, 12505834, + 12505578, 12440042, 12374250, 12308714, 12308458, 12242922, 12177130, 12111593, 12111337, 12045801, 11980009, + 11914473, 11914217, 11848680, 11783144, 11717352, 11717352, 11651560, 11586024, 11520232, 11520231, 11454439, + 11388903, 11323111, 11323111, 11257318, 11191782, 11126246, 11125990, 11060454, 10994661, 10929125, 10928869, + 10863333, 10797541, 10797540, 10731748, 10666212, 10600420, 10600419, 10534627, 10469091, 10469091, 10403298, + 10337762, 10337506, 10271969, 10206177, 10206177, 10140384, 10140384, 10074592, 10009055, 10008799, 9943263, + 9943006, 9877470, 9811934, 9811677, 9746141, 9745884, 9680348, 9614556, 9614555, 9548763, 9548762, 9482970, + 9417434, 9417177, 9351641, 9351385, 9285848, 9285592, 9220055, 9154519, 9154262, 9088726, 9088470, 9022933, + 9022677, 8957140, 8956884, 8891348, 8891091, 8825555, 8825298, 8759762, 8693969, 8693969, 8628176, 8628176, + 8562639, 8562383, 8496847, 8496590, 8431054, 8430797, 8365261, 8365004, 8299468, 8299211, 8233675, 8233418, + 8167882, 8167625, 8167625, 8102088, 8101832, 8036295, 7970503, 7970503, 7904710, 7904710, 7838917, 7838917, + 7773124, 7773124, 7707332, 7641795, 7641795, 7576003, 7576002, 7510210, 7444674, 7444417, 7378881, 7378625, + 7313088, 7247296, 7247296, 7181759, 7115967, 7115967, 7050174, 6984638, 6984382, 6918845, 6853053, 6787517, + 6787517, 6721724, 6656188, 6655932, 6590395, 6524603, 6459067, 6458811, 6393274, 6327738, 6261946, 6261946, + 6196153, 6130617, 6064825, 5999289, 5999288, 5933496, 5867960, 5802168, 5736631, 5736375, 5670839, 5605047, + 5539511, 5473974, 5408182, 5342646, 5276854, 5276853, 5211061, 5145525, 5079989, 5014197, 4948660, 4882868, + 4817332, 4751540, 4686004, 4620467, 4554675, 4489139, 4423347, 4357811, 4292274, 4226482, 4160946, 4095154, + 3964082, 3898290, 3832753 + }; + public static final Colorcet BLUES = new Colorcet(blues, FAMILY.LINEAR); - private static final int[] bmw = { - 1112, 1370, 1372, 1631, 1889, 1892, 2150, 2152, 2155, 2413, 2415, 2418, 2420, 2679, 2681, 2683, - 2942, 2944, 2947, 2949, 3208, 3210, 3213, 3471, 3474, 3476, 3735, 3737, 3740, 3998, 4001, 4003, - 4262, 4264, 4267, 4525, 4528, 4530, 4789, 4791, 5050, 5052, 5055, 5313, 5316, 5574, 5576, 5835, - 5837, 5839, 6097, 6099, 6101, 6359, 6361, 6619, 6621, 6623, 6625, 6882, 6884, 6886, 7143, 7145, - 7146, 7147, 7405, 7406, 7407, 7408, 7666, 7667, 7668, 7669, 7670, 7927, 7927, 7928, 7929, 7930, - 7931, 7931, 8188, 663549, 1581053, 2170878, 2695166, 3088383, 3481599, 3874815, 4202495, - 4530175, 4792319, 5119999, 5382143, 5644287, 5906431, 6168575, 6430719, 6627327, 6889471, - 7086079, 7348223, 7544831, 7806975, 8003327, 8199935, 8396543, 8593151, 8789759, 8986367, - 9182975, 9379583, 9510655, 9707263, 9903871, 10100479, 10231551, 10428159, 10559231, 10755839, - 10952447, 11083519, 11280127, 11411199, 11607807, 11738879, 11869951, 12066559, 12197631, - 12394239, 12525311, 12656383, 12787711, 12918783, 13115647, 13246975, 13378047, 13509375, - 13640703, 13706495, 13837823, 13969151, 14100479, 14231807, 14297599, 14429183, 14560511, - 14626303, 14757887, 14889215, 14955263, 15086591, 15152639, 15218431, 15350015, 15415807, - 15481855, 15613439, 15679487, 15745279, 15811327, 15877375, 15943423, 16009471, 16075519, - 16141567, 16207615, 16273663, 16339711, 16340223, 16406271, 16472319, 16472831, 16538879, - 16539391, 16605439, 16605951, 16671999, 16672767, 16673279, 16673791, 16739839, 16740351, - 16740863, 16741631, 16742143, 16742655, 16743167, 16743679, 16744191, 16744703, 16745215, - 16745727, 16746239, 16746751, 16747263, 16747775, 16748287, 16748543, 16749055, 16749567, - 16750079, 16750591, 16750847, 16751359, 16751871, 16752383, 16752639, 16753151, 16753663, - 16754175, 16754431, 16754943, 16755455, 16755711, 16756223, 16756735, 16756991, 16757503, - 16758015, 16758271, 16758783, 16759039, 16759551, 16760063, 16760319, 16760831, 16761087, - 16761599, 16762111, 16762367, 16762879, 16763135, 16763647, 16763903, 16764415, 16764927, - 16765183, 16765695, 16765951, 16766463, 16766719, 16767231, 16767487, 16767999, 16768511, - 16768766, 16769278, 16769534, 16770046, 16704766, 16705278, 16705534, 16706046, 16706302 - }; - public static final Colorcet BMW = new Colorcet(bmw, FAMILY.LINEAR); + private static final int[] bmw = { + 1112, 1370, 1372, 1631, 1889, 1892, 2150, 2152, 2155, 2413, 2415, 2418, 2420, 2679, 2681, 2683, 2942, 2944, + 2947, 2949, 3208, 3210, 3213, 3471, 3474, 3476, 3735, 3737, 3740, 3998, 4001, 4003, 4262, 4264, 4267, 4525, + 4528, 4530, 4789, 4791, 5050, 5052, 5055, 5313, 5316, 5574, 5576, 5835, 5837, 5839, 6097, 6099, 6101, 6359, + 6361, 6619, 6621, 6623, 6625, 6882, 6884, 6886, 7143, 7145, 7146, 7147, 7405, 7406, 7407, 7408, 7666, 7667, + 7668, 7669, 7670, 7927, 7927, 7928, 7929, 7930, 7931, 7931, 8188, 663549, 1581053, 2170878, 2695166, 3088383, + 3481599, 3874815, 4202495, 4530175, 4792319, 5119999, 5382143, 5644287, 5906431, 6168575, 6430719, 6627327, + 6889471, 7086079, 7348223, 7544831, 7806975, 8003327, 8199935, 8396543, 8593151, 8789759, 8986367, 9182975, + 9379583, 9510655, 9707263, 9903871, 10100479, 10231551, 10428159, 10559231, 10755839, 10952447, 11083519, + 11280127, 11411199, 11607807, 11738879, 11869951, 12066559, 12197631, 12394239, 12525311, 12656383, 12787711, + 12918783, 13115647, 13246975, 13378047, 13509375, 13640703, 13706495, 13837823, 13969151, 14100479, 14231807, + 14297599, 14429183, 14560511, 14626303, 14757887, 14889215, 14955263, 15086591, 15152639, 15218431, 15350015, + 15415807, 15481855, 15613439, 15679487, 15745279, 15811327, 15877375, 15943423, 16009471, 16075519, 16141567, + 16207615, 16273663, 16339711, 16340223, 16406271, 16472319, 16472831, 16538879, 16539391, 16605439, 16605951, + 16671999, 16672767, 16673279, 16673791, 16739839, 16740351, 16740863, 16741631, 16742143, 16742655, 16743167, + 16743679, 16744191, 16744703, 16745215, 16745727, 16746239, 16746751, 16747263, 16747775, 16748287, 16748543, + 16749055, 16749567, 16750079, 16750591, 16750847, 16751359, 16751871, 16752383, 16752639, 16753151, 16753663, + 16754175, 16754431, 16754943, 16755455, 16755711, 16756223, 16756735, 16756991, 16757503, 16758015, 16758271, + 16758783, 16759039, 16759551, 16760063, 16760319, 16760831, 16761087, 16761599, 16762111, 16762367, 16762879, + 16763135, 16763647, 16763903, 16764415, 16764927, 16765183, 16765695, 16765951, 16766463, 16766719, 16767231, + 16767487, 16767999, 16768511, 16768766, 16769278, 16769534, 16770046, 16704766, 16705278, 16705534, 16706046, + 16706302 + }; + public static final Colorcet BMW = new Colorcet(bmw, FAMILY.LINEAR); - private static final int[] bmy = { - 3196, 3198, 3456, 3458, 3715, 3717, 3719, 3977, 3978, 4236, 4238, 4239, 4497, 4498, 4500, 4757, - 4759, 4760, 5018, 5019, 5020, 5277, 5279, 5280, 5281, 5538, 5539, 5540, 5541, 5541, 5542, 5543, - 5544, 5544, 5544, 5545, 660905, 1316265, 1840553, 2299305, 2692521, 3019945, 3413160, 3740584, - 4068263, 4330150, 4657829, 4919716, 5181859, 5443746, 5640353, 5902240, 6098591, 6295198, - 6491549, 6688156, 6884507, 7080859, 7277466, 7408281, 7604888, 7735703, 7932055, 8063126, - 8193941, 8390549, 8521364, 8652435, 8783251, 8914322, 9110930, 9241745, 9372817, 9503888, - 9634704, 9765775, 9896847, 10027662, 10158734, 10289805, 10420621, 10551693, 10617228, 10748044, - 10879115, 11010187, 11141258, 11272330, 11403401, 11534473, 11665544, 11731080, 11862151, - 11993223, 12124295, 12255366, 12386438, 12517509, 12583045, 12714116, 12845188, 12976259, - 13107331, 13172866, 13303938, 13435009, 13566081, 13697152, 13762688, 13893759, 14024831, - 14155902, 14221438, 14352509, 14418044, 14549116, 14680187, 14746235, 14877818, 14943866, - 15075449, 15141496, 15207800, 15339383, 15405431, 15537270, 15603317, 15669365, 15800948, - 15866996, 15933043, 15999090, 16065138, 16196721, 16262768, 16328816, 16394863, 16460910, - 16526957, 16593005, 16659052, 16725099, 16725611, 16726122, 16726633, 16727144, 16727655, - 16728167, 16728678, 16729189, 16729956, 16730468, 16730979, 16731490, 16732001, 16732512, - 16733023, 16733791, 16734302, 16734813, 16735324, 16735835, 16736346, 16737113, 16737624, - 16738135, 16738646, 16739157, 16739668, 16740179, 16740690, 16741201, 16741712, 16742223, - 16742734, 16743245, 16743756, 16744267, 16744778, 16745288, 16745543, 16746054, 16746565, - 16747076, 16747586, 16747841, 16748352, 16748862, 16749117, 16749628, 16750138, 16750393, - 16750903, 16751414, 16751668, 16752179, 16752689, 16752944, 16753454, 16753709, 16754220, - 16754475, 16754986, 16755241, 16755752, 16756007, 16756518, 16756773, 16757284, 16757540, - 16758051, 16758306, 16758562, 16759073, 16759328, 16759840, 16760095, 16760351, 16760863, - 16761118, 16761374, 16761885, 16762141, 16762653, 16762909, 16763164, 16763676, 16763932, - 16764188, 16764700, 16764956, 16765212, 16765724, 16765980, 16766236, 16766748, 16767004, - 16767260, 16767772, 16768029, 16768285, 16768541, 16769053, 16769310, 16769566, 16770078, - 16770335, 16770591, 16771103, 16771360, 16771616, 16772129, 16772385, 16772642, 16772898, - 16773411 - }; - public static final Colorcet BMY = new Colorcet(bmy, FAMILY.LINEAR); + private static final int[] bmy = { + 3196, 3198, 3456, 3458, 3715, 3717, 3719, 3977, 3978, 4236, 4238, 4239, 4497, 4498, 4500, 4757, 4759, 4760, + 5018, 5019, 5020, 5277, 5279, 5280, 5281, 5538, 5539, 5540, 5541, 5541, 5542, 5543, 5544, 5544, 5544, 5545, + 660905, 1316265, 1840553, 2299305, 2692521, 3019945, 3413160, 3740584, 4068263, 4330150, 4657829, 4919716, + 5181859, 5443746, 5640353, 5902240, 6098591, 6295198, 6491549, 6688156, 6884507, 7080859, 7277466, 7408281, + 7604888, 7735703, 7932055, 8063126, 8193941, 8390549, 8521364, 8652435, 8783251, 8914322, 9110930, 9241745, + 9372817, 9503888, 9634704, 9765775, 9896847, 10027662, 10158734, 10289805, 10420621, 10551693, 10617228, + 10748044, 10879115, 11010187, 11141258, 11272330, 11403401, 11534473, 11665544, 11731080, 11862151, 11993223, + 12124295, 12255366, 12386438, 12517509, 12583045, 12714116, 12845188, 12976259, 13107331, 13172866, 13303938, + 13435009, 13566081, 13697152, 13762688, 13893759, 14024831, 14155902, 14221438, 14352509, 14418044, 14549116, + 14680187, 14746235, 14877818, 14943866, 15075449, 15141496, 15207800, 15339383, 15405431, 15537270, 15603317, + 15669365, 15800948, 15866996, 15933043, 15999090, 16065138, 16196721, 16262768, 16328816, 16394863, 16460910, + 16526957, 16593005, 16659052, 16725099, 16725611, 16726122, 16726633, 16727144, 16727655, 16728167, 16728678, + 16729189, 16729956, 16730468, 16730979, 16731490, 16732001, 16732512, 16733023, 16733791, 16734302, 16734813, + 16735324, 16735835, 16736346, 16737113, 16737624, 16738135, 16738646, 16739157, 16739668, 16740179, 16740690, + 16741201, 16741712, 16742223, 16742734, 16743245, 16743756, 16744267, 16744778, 16745288, 16745543, 16746054, + 16746565, 16747076, 16747586, 16747841, 16748352, 16748862, 16749117, 16749628, 16750138, 16750393, 16750903, + 16751414, 16751668, 16752179, 16752689, 16752944, 16753454, 16753709, 16754220, 16754475, 16754986, 16755241, + 16755752, 16756007, 16756518, 16756773, 16757284, 16757540, 16758051, 16758306, 16758562, 16759073, 16759328, + 16759840, 16760095, 16760351, 16760863, 16761118, 16761374, 16761885, 16762141, 16762653, 16762909, 16763164, + 16763676, 16763932, 16764188, 16764700, 16764956, 16765212, 16765724, 16765980, 16766236, 16766748, 16767004, + 16767260, 16767772, 16768029, 16768285, 16768541, 16769053, 16769310, 16769566, 16770078, 16770335, 16770591, + 16771103, 16771360, 16771616, 16772129, 16772385, 16772642, 16772898, 16773411 + }; + public static final Colorcet BMY = new Colorcet(bmy, FAMILY.LINEAR); - private static final int[] bwy = { - 3838206, 4035070, 4231678, 4428541, 4625405, 4756477, 4953341, 5084669, 5281277, 5412605, - 5543933, 5740541, 5871869, 6003197, 6134525, 6265597, 6396924, 6528252, 6659324, 6790652, - 6921980, 6987772, 7118844, 7250172, 7381500, 7447036, 7578364, 7709692, 7775483, 7906555, - 7972347, 8103675, 8234747, 8300539, 8431867, 8497659, 8628731, 8694523, 8825851, 8891386, - 9022714, 9088506, 9154298, 9285370, 9351162, 9482490, 9548282, 9613818, 9745146, 9810938, - 9876729, 10007801, 10073593, 10139385, 10270713, 10336249, 10402041, 10533369, 10598905, - 10664697, 10730488, 10861816, 10927352, 10993144, 11058936, 11190264, 11256056, 11321592, - 11387384, 11518711, 11584503, 11650039, 11715831, 11781623, 11912951, 11978487, 12044279, - 12110071, 12175863, 12241398, 12372726, 12438518, 12504310, 12569846, 12635638, 12701430, - 12767222, 12898549, 12964085, 13029877, 13095669, 13161461, 13226997, 13292789, 13358581, - 13489909, 13555700, 13621236, 13687028, 13752820, 13818612, 13884148, 13949940, 14015732, - 14081523, 14147315, 14278387, 14344179, 14409971, 14475763, 14541299, 14607090, 14672882, - 14738674, 14804466, 14870002, 14935794, 15001586, 15067377, 15132913, 15198705, 15264497, - 15330032, 15395824, 15461359, 15461615, 15527150, 15527149, 15592684, 15592683, 15592681, - 15592424, 15592422, 15592165, 15592163, 15526370, 15526112, 15526110, 15460317, 15460059, - 15459801, 15394264, 15394006, 15328212, 15327954, 15262161, 15262159, 15261901, 15196108, - 15195850, 15130056, 15130055, 15064261, 15064003, 14998210, 14998208, 14932414, 14932157, - 14866363, 14866105, 14866104, 14800310, 14800052, 14734258, 14734257, 14668463, 14668206, - 14602412, 14602154, 14536617, 14536359, 14470565, 14404772, 14404770, 14338976, 14338719, - 14272925, 14272667, 14207130, 14206872, 14141078, 14140821, 14075283, 14075025, 14009232, - 13943438, 13943436, 13877643, 13877385, 13811592, 13811590, 13745796, 13745539, 13679745, - 13614207, 13613950, 13548156, 13547898, 13482361, 13416567, 13416309, 13350516, 13350514, - 13284721, 13218927, 13218669, 13153132, 13152874, 13087080, 13021287, 13021285, 12955491, - 12889698, 12889440, 12823902, 12823645, 12757851, 12692057, 12692056, 12626262, 12560468, - 12560211, 12494673, 12428879, 12428622, 12363084, 12297290, 12297032, 12231239, 12165701, - 12165443, 12099649, 12033856, 12033854, 11968060, 11902266, 11902264, 11836470, 11770676, - 11770418, 11704880, 11639086, 11573292, 11573290, 11507496, 11441702, 11441444, 11375905, - 11310111, 11309853, 11244314, 11178519, 11112724, 11112464, 11046925, 10981128 - }; - public static final Colorcet BWY = new Colorcet(bwy, FAMILY.DIVERGENT); + private static final int[] bwy = { + 3838206, 4035070, 4231678, 4428541, 4625405, 4756477, 4953341, 5084669, 5281277, 5412605, 5543933, 5740541, + 5871869, 6003197, 6134525, 6265597, 6396924, 6528252, 6659324, 6790652, 6921980, 6987772, 7118844, 7250172, + 7381500, 7447036, 7578364, 7709692, 7775483, 7906555, 7972347, 8103675, 8234747, 8300539, 8431867, 8497659, + 8628731, 8694523, 8825851, 8891386, 9022714, 9088506, 9154298, 9285370, 9351162, 9482490, 9548282, 9613818, + 9745146, 9810938, 9876729, 10007801, 10073593, 10139385, 10270713, 10336249, 10402041, 10533369, 10598905, + 10664697, 10730488, 10861816, 10927352, 10993144, 11058936, 11190264, 11256056, 11321592, 11387384, 11518711, + 11584503, 11650039, 11715831, 11781623, 11912951, 11978487, 12044279, 12110071, 12175863, 12241398, 12372726, + 12438518, 12504310, 12569846, 12635638, 12701430, 12767222, 12898549, 12964085, 13029877, 13095669, 13161461, + 13226997, 13292789, 13358581, 13489909, 13555700, 13621236, 13687028, 13752820, 13818612, 13884148, 13949940, + 14015732, 14081523, 14147315, 14278387, 14344179, 14409971, 14475763, 14541299, 14607090, 14672882, 14738674, + 14804466, 14870002, 14935794, 15001586, 15067377, 15132913, 15198705, 15264497, 15330032, 15395824, 15461359, + 15461615, 15527150, 15527149, 15592684, 15592683, 15592681, 15592424, 15592422, 15592165, 15592163, 15526370, + 15526112, 15526110, 15460317, 15460059, 15459801, 15394264, 15394006, 15328212, 15327954, 15262161, 15262159, + 15261901, 15196108, 15195850, 15130056, 15130055, 15064261, 15064003, 14998210, 14998208, 14932414, 14932157, + 14866363, 14866105, 14866104, 14800310, 14800052, 14734258, 14734257, 14668463, 14668206, 14602412, 14602154, + 14536617, 14536359, 14470565, 14404772, 14404770, 14338976, 14338719, 14272925, 14272667, 14207130, 14206872, + 14141078, 14140821, 14075283, 14075025, 14009232, 13943438, 13943436, 13877643, 13877385, 13811592, 13811590, + 13745796, 13745539, 13679745, 13614207, 13613950, 13548156, 13547898, 13482361, 13416567, 13416309, 13350516, + 13350514, 13284721, 13218927, 13218669, 13153132, 13152874, 13087080, 13021287, 13021285, 12955491, 12889698, + 12889440, 12823902, 12823645, 12757851, 12692057, 12692056, 12626262, 12560468, 12560211, 12494673, 12428879, + 12428622, 12363084, 12297290, 12297032, 12231239, 12165701, 12165443, 12099649, 12033856, 12033854, 11968060, + 11902266, 11902264, 11836470, 11770676, 11770418, 11704880, 11639086, 11573292, 11573290, 11507496, 11441702, + 11441444, 11375905, 11310111, 11309853, 11244314, 11178519, 11112724, 11112464, 11046925, 10981128 + }; + public static final Colorcet BWY = new Colorcet(bwy, FAMILY.DIVERGENT); - private static final int[] colorwheel = { - 3023338, 3153899, 3350252, 3611886, 3808239, 4070127, 4266736, 4528881, 4791026, 4987890, - 5250291, 5447155, 5709556, 5906420, 6103541, 6300405, 6497525, 6694390, 6891510, 7088374, - 7285495, 7482359, 7679480, 7876344, 8007672, 8204792, 8401657, 8598521, 8730105, 8926970, - 9123834, 9320698, 9517562, 9714426, 9911290, 10108155, 10305019, 10501627, 10764027, 10960891, - 11157499, 11419899, 11616507, 11813371, 12009979, 12272378, 12468986, 12665850, 12927994, - 13124858, 13321466, 13518330, 13714937, 13911801, 14108409, 14305273, 14502136, 14699000, - 14830327, 15027190, 15158774, 15290357, 15421684, 15553267, 15685105, 15751152, 15882734, - 15949036, 16015339, 16081641, 16147942, 16214244, 16215010, 16281312, 16347613, 16348379, - 16349144, 16415446, 16416467, 16417232, 16483534, 16484299, 16484808, 16485573, 16486339, - 16487104, 16487869, 16488634, 16489144, 16489909, 16490674, 16491183, 16491949, 16492714, - 16493223, 16493988, 16494497, 16495262, 16495772, 16496537, 16497046, 16497555, 16498320, - 16498829, 16499338, 16499847, 16500612, 16501121, 16501630, 16567675, 16568183, 16568948, - 16569457, 16569966, 16570474, 16505447, 16505955, 16506463, 16506972, 16441944, 16442452, - 16377168, 16312140, 16246856, 16181828, 16116544, 15985724, 15920441, 15789365, 15658545, - 15527470, 15396395, 15265320, 15068709, 14937378, 14740768, 14543902, 14412572, 14215963, - 14019098, 13822233, 13625368, 13428503, 13231638, 13034774, 12903445, 12706581, 12509716, - 12312596, 12115732, 11918867, 11722003, 11525139, 11328274, 11131410, 10934546, 10737681, - 10540817, 10278161, 10081296, 9884432, 9687568, 9490703, 9293839, 9096975, 8834574, 8637710, - 8440590, 8243725, 7981325, 7784461, 7587596, 7325196, 7128332, 6865676, 6668811, 6406411, - 6144011, 5947148, 5684748, 5422092, 5225229, 4962830, 4700432, 4503313, 4240915, 4044054, - 3846936, 3715611, 3518750, 3452705, 3321380, 3255336, 3255083, 3254575, 3319603, 3384886, - 3449914, 3580478, 3645506, 3776070, 3841354, 3971917, 4036945, 4101973, 4232537, 4297564, - 4362592, 4427876, 4427367, 4492395, 4491886, 4556914, 4556405, 4555897, 4555388, 4489344, - 4489091, 4423046, 4422538, 4356493, 4290448, 4224404, 4092823, 4026778, 3960733, 3829152, - 3763108, 3631527, 3499946, 3433901, 3302320, 3236275, 3104693, 3038648, 2972347, 2906302, - 2840000, 2773955, 2707653, 2641608, 2640842, 2574541, 2508495, 2507730, 2441428, 2440662, - 2374361, 2373595, 2372829, 2372063, 2436833, 2501603, 2566629, 2696935, 2827496 - }; - public static final Colorcet COLORWHEEL = new Colorcet(colorwheel, FAMILY.CYCLIC); + private static final int[] colorwheel = { + 3023338, 3153899, 3350252, 3611886, 3808239, 4070127, 4266736, 4528881, 4791026, 4987890, 5250291, 5447155, + 5709556, 5906420, 6103541, 6300405, 6497525, 6694390, 6891510, 7088374, 7285495, 7482359, 7679480, 7876344, + 8007672, 8204792, 8401657, 8598521, 8730105, 8926970, 9123834, 9320698, 9517562, 9714426, 9911290, 10108155, + 10305019, 10501627, 10764027, 10960891, 11157499, 11419899, 11616507, 11813371, 12009979, 12272378, 12468986, + 12665850, 12927994, 13124858, 13321466, 13518330, 13714937, 13911801, 14108409, 14305273, 14502136, 14699000, + 14830327, 15027190, 15158774, 15290357, 15421684, 15553267, 15685105, 15751152, 15882734, 15949036, 16015339, + 16081641, 16147942, 16214244, 16215010, 16281312, 16347613, 16348379, 16349144, 16415446, 16416467, 16417232, + 16483534, 16484299, 16484808, 16485573, 16486339, 16487104, 16487869, 16488634, 16489144, 16489909, 16490674, + 16491183, 16491949, 16492714, 16493223, 16493988, 16494497, 16495262, 16495772, 16496537, 16497046, 16497555, + 16498320, 16498829, 16499338, 16499847, 16500612, 16501121, 16501630, 16567675, 16568183, 16568948, 16569457, + 16569966, 16570474, 16505447, 16505955, 16506463, 16506972, 16441944, 16442452, 16377168, 16312140, 16246856, + 16181828, 16116544, 15985724, 15920441, 15789365, 15658545, 15527470, 15396395, 15265320, 15068709, 14937378, + 14740768, 14543902, 14412572, 14215963, 14019098, 13822233, 13625368, 13428503, 13231638, 13034774, 12903445, + 12706581, 12509716, 12312596, 12115732, 11918867, 11722003, 11525139, 11328274, 11131410, 10934546, 10737681, + 10540817, 10278161, 10081296, 9884432, 9687568, 9490703, 9293839, 9096975, 8834574, 8637710, 8440590, 8243725, + 7981325, 7784461, 7587596, 7325196, 7128332, 6865676, 6668811, 6406411, 6144011, 5947148, 5684748, 5422092, + 5225229, 4962830, 4700432, 4503313, 4240915, 4044054, 3846936, 3715611, 3518750, 3452705, 3321380, 3255336, + 3255083, 3254575, 3319603, 3384886, 3449914, 3580478, 3645506, 3776070, 3841354, 3971917, 4036945, 4101973, + 4232537, 4297564, 4362592, 4427876, 4427367, 4492395, 4491886, 4556914, 4556405, 4555897, 4555388, 4489344, + 4489091, 4423046, 4422538, 4356493, 4290448, 4224404, 4092823, 4026778, 3960733, 3829152, 3763108, 3631527, + 3499946, 3433901, 3302320, 3236275, 3104693, 3038648, 2972347, 2906302, 2840000, 2773955, 2707653, 2641608, + 2640842, 2574541, 2508495, 2507730, 2441428, 2440662, 2374361, 2373595, 2372829, 2372063, 2436833, 2501603, + 2566629, 2696935, 2827496 + }; + public static final Colorcet COLORWHEEL = new Colorcet(colorwheel, FAMILY.CYCLIC); - private static final int[] coolwarm = { - 2117850, 2445787, 2708187, 2970587, 3233243, 3430107, 3692508, 3889372, 4020700, 4217564, - 4414429, 4545757, 4742621, 4874205, 5071069, 5202398, 5333726, 5465054, 5661918, 5793246, - 5924831, 6056159, 6187487, 6318815, 6384608, 6515936, 6647520, 6778848, 6910176, 7041505, - 7107297, 7238881, 7370209, 7436001, 7567330, 7698658, 7764450, 7896034, 8027362, 8093154, - 8224483, 8290275, 8421859, 8553187, 8618979, 8750308, 8816356, 8947684, 9013476, 9144804, - 9210597, 9342181, 9407973, 9473765, 9605093, 9671141, 9802470, 9868262, 9999590, 10065638, - 10131430, 10262759, 10328551, 10394599, 10525927, 10591719, 10723047, 10789096, 10854888, - 10986216, 11052008, 11118056, 11249384, 11315177, 11380969, 11512553, 11578345, 11644137, - 11710185, 11841514, 11907306, 11973098, 12104682, 12170474, 12236266, 12302314, 12433643, - 12499435, 12565227, 12631275, 12762603, 12828395, 12894444, 12960236, 13091564, 13157612, - 13223404, 13289196, 13420780, 13486572, 13552365, 13618157, 13749741, 13815533, 13881325, - 13947373, 14013165, 14144494, 14210542, 14276334, 14342126, 14408174, 14539502, 14605294, - 14671342, 14737134, 14802926, 14934254, 15000046, 15065838, 15131630, 15197422, 15263213, - 15329005, 15394540, 15460332, 15525867, 15525866, 15591401, 15656680, 15656678, 15721957, - 15721956, 15787234, 15786976, 15786719, 15851741, 15851483, 15851225, 15850711, 15850454, - 15849940, 15849682, 15849168, 15848910, 15848396, 15848138, 15847624, 15847366, 15846852, - 15846594, 15846080, 15845823, 15845309, 15844795, 15844537, 15778487, 15778229, 15777715, - 15777457, 15776943, 15776685, 15710636, 15710122, 15709864, 15709350, 15709092, 15643042, - 15642784, 15642270, 15642013, 15575963, 15575449, 15575191, 15574677, 15508883, 15508369, - 15508112, 15442062, 15441804, 15441290, 15375240, 15374983, 15374469, 15308675, 15308161, - 15307903, 15241853, 15241340, 15241082, 15175032, 15174774, 15108725, 15108467, 15042417, - 15041903, 15041645, 14975596, 14975338, 14909288, 14908774, 14842981, 14842467, 14776673, - 14776159, 14710110, 14709852, 14643802, 14643544, 14577495, 14576981, 14511187, 14510674, - 14444624, 14378830, 14378316, 14312267, 14312009, 14245959, 14245446, 14179652, 14113602, - 14113088, 14047039, 13981245, 13980731, 13914682, 13914168, 13848118, 13782325, 13781811, - 13715761, 13649712, 13649198, 13583148, 13517098, 13516585, 13450535, 13384485, 13318436, - 13317922, 13251616, 13185566, 13185053, 13118747, 13052697, 12986391, 12985621, 12919316, - 12853010, 12786704, 12785678, 12719116, 12652297, 12585223, 12517893 - }; - public static final Colorcet COOLWARM = new Colorcet(coolwarm, FAMILY.DIVERGENT); + private static final int[] coolwarm = { + 2117850, 2445787, 2708187, 2970587, 3233243, 3430107, 3692508, 3889372, 4020700, 4217564, 4414429, 4545757, + 4742621, 4874205, 5071069, 5202398, 5333726, 5465054, 5661918, 5793246, 5924831, 6056159, 6187487, 6318815, + 6384608, 6515936, 6647520, 6778848, 6910176, 7041505, 7107297, 7238881, 7370209, 7436001, 7567330, 7698658, + 7764450, 7896034, 8027362, 8093154, 8224483, 8290275, 8421859, 8553187, 8618979, 8750308, 8816356, 8947684, + 9013476, 9144804, 9210597, 9342181, 9407973, 9473765, 9605093, 9671141, 9802470, 9868262, 9999590, 10065638, + 10131430, 10262759, 10328551, 10394599, 10525927, 10591719, 10723047, 10789096, 10854888, 10986216, 11052008, + 11118056, 11249384, 11315177, 11380969, 11512553, 11578345, 11644137, 11710185, 11841514, 11907306, 11973098, + 12104682, 12170474, 12236266, 12302314, 12433643, 12499435, 12565227, 12631275, 12762603, 12828395, 12894444, + 12960236, 13091564, 13157612, 13223404, 13289196, 13420780, 13486572, 13552365, 13618157, 13749741, 13815533, + 13881325, 13947373, 14013165, 14144494, 14210542, 14276334, 14342126, 14408174, 14539502, 14605294, 14671342, + 14737134, 14802926, 14934254, 15000046, 15065838, 15131630, 15197422, 15263213, 15329005, 15394540, 15460332, + 15525867, 15525866, 15591401, 15656680, 15656678, 15721957, 15721956, 15787234, 15786976, 15786719, 15851741, + 15851483, 15851225, 15850711, 15850454, 15849940, 15849682, 15849168, 15848910, 15848396, 15848138, 15847624, + 15847366, 15846852, 15846594, 15846080, 15845823, 15845309, 15844795, 15844537, 15778487, 15778229, 15777715, + 15777457, 15776943, 15776685, 15710636, 15710122, 15709864, 15709350, 15709092, 15643042, 15642784, 15642270, + 15642013, 15575963, 15575449, 15575191, 15574677, 15508883, 15508369, 15508112, 15442062, 15441804, 15441290, + 15375240, 15374983, 15374469, 15308675, 15308161, 15307903, 15241853, 15241340, 15241082, 15175032, 15174774, + 15108725, 15108467, 15042417, 15041903, 15041645, 14975596, 14975338, 14909288, 14908774, 14842981, 14842467, + 14776673, 14776159, 14710110, 14709852, 14643802, 14643544, 14577495, 14576981, 14511187, 14510674, 14444624, + 14378830, 14378316, 14312267, 14312009, 14245959, 14245446, 14179652, 14113602, 14113088, 14047039, 13981245, + 13980731, 13914682, 13914168, 13848118, 13782325, 13781811, 13715761, 13649712, 13649198, 13583148, 13517098, + 13516585, 13450535, 13384485, 13318436, 13317922, 13251616, 13185566, 13185053, 13118747, 13052697, 12986391, + 12985621, 12919316, 12853010, 12786704, 12785678, 12719116, 12652297, 12585223, 12517893 + }; + public static final Colorcet COOLWARM = new Colorcet(coolwarm, FAMILY.DIVERGENT); - private static final int[] cwr = { - 2738662, 3000807, 3263207, 3525351, 3787751, 3984359, 4180967, 4377831, 4574440, 4771304, - 4902376, 5098984, 5295848, 5426920, 5557992, 5754857, 5885929, 6017257, 6148329, 6279401, - 6410729, 6541801, 6672874, 6804202, 6935274, 7066602, 7197674, 7328746, 7460074, 7591147, - 7656683, 7788011, 7919083, 8050411, 8115947, 8247019, 8378348, 8443884, 8574956, 8706284, - 8771820, 8903148, 8968684, 9099757, 9165549, 9296621, 9362157, 9493485, 9559021, 9690349, - 9755886, 9886958, 9952750, 10083822, 10149358, 10280686, 10346222, 10477551, 10543087, 10608623, - 10739951, 10805487, 10936559, 11002351, 11067888, 11199216, 11264752, 11330288, 11461616, - 11527152, 11592688, 11724017, 11789553, 11855089, 11986417, 12051953, 12117745, 12248817, - 12314354, 12380146, 12511218, 12576754, 12642546, 12708082, 12839410, 12904947, 12970483, - 13101811, 13167347, 13232883, 13298675, 13429747, 13495284, 13561076, 13626612, 13757940, - 13823476, 13889012, 13954804, 14085877, 14151413, 14217205, 14282741, 14348533, 14479605, - 14545141, 14610934, 14676470, 14742006, 14873334, 14938870, 15004406, 15070198, 15135735, - 15267063, 15332599, 15398135, 15463927, 15529463, 15660535, 15726327, 15791863, 15857399, - 15922935, 15988471, 16054007, 16119543, 16185079, 16250614, 16250614, 16315893, 16315893, - 16381428, 16381171, 16380915, 16446450, 16446193, 16446192, 16445935, 16445679, 16511214, - 16510957, 16510700, 16510443, 16510443, 16510186, 16509929, 16509928, 16575207, 16574950, - 16574950, 16574693, 16574436, 16574435, 16574178, 16573922, 16573921, 16573664, 16638943, - 16638686, 16638686, 16638429, 16638172, 16638171, 16637914, 16637658, 16637657, 16637400, - 16637143, 16637142, 16636886, 16702165, 16702164, 16701907, 16701650, 16701394, 16701393, - 16701136, 16700879, 16700878, 16700622, 16700365, 16700364, 16700107, 16699850, 16699850, - 16699593, 16699336, 16699335, 16699078, 16698822, 16698565, 16698564, 16698307, 16698051, - 16698050, 16697793, 16697536, 16697535, 16762815, 16762558, 16762557, 16762300, 16762043, - 16761787, 16761786, 16761529, 16761272, 16761272, 16761015, 16695222, 16695221, 16694964, - 16694708, 16694707, 16694450, 16694193, 16693937, 16693936, 16693679, 16693422, 16693421, - 16693165, 16692908, 16692907, 16692650, 16692394, 16692393, 16692136, 16691879, 16691623, - 16691622, 16691365, 16691108, 16691107, 16690851, 16690594, 16690593, 16690336, 16624544, - 16624287, 16624286, 16624029, 16623773, 16623772, 16623515, 16623258, 16623258, 16623001, - 16622744, 16622487, 16622487, 16622230, 16621973, 16556436, 16556180 - }; - public static final Colorcet CWR = new Colorcet(cwr, FAMILY.DIVERGENT); + private static final int[] cwr = { + 2738662, 3000807, 3263207, 3525351, 3787751, 3984359, 4180967, 4377831, 4574440, 4771304, 4902376, 5098984, + 5295848, 5426920, 5557992, 5754857, 5885929, 6017257, 6148329, 6279401, 6410729, 6541801, 6672874, 6804202, + 6935274, 7066602, 7197674, 7328746, 7460074, 7591147, 7656683, 7788011, 7919083, 8050411, 8115947, 8247019, + 8378348, 8443884, 8574956, 8706284, 8771820, 8903148, 8968684, 9099757, 9165549, 9296621, 9362157, 9493485, + 9559021, 9690349, 9755886, 9886958, 9952750, 10083822, 10149358, 10280686, 10346222, 10477551, 10543087, + 10608623, 10739951, 10805487, 10936559, 11002351, 11067888, 11199216, 11264752, 11330288, 11461616, 11527152, + 11592688, 11724017, 11789553, 11855089, 11986417, 12051953, 12117745, 12248817, 12314354, 12380146, 12511218, + 12576754, 12642546, 12708082, 12839410, 12904947, 12970483, 13101811, 13167347, 13232883, 13298675, 13429747, + 13495284, 13561076, 13626612, 13757940, 13823476, 13889012, 13954804, 14085877, 14151413, 14217205, 14282741, + 14348533, 14479605, 14545141, 14610934, 14676470, 14742006, 14873334, 14938870, 15004406, 15070198, 15135735, + 15267063, 15332599, 15398135, 15463927, 15529463, 15660535, 15726327, 15791863, 15857399, 15922935, 15988471, + 16054007, 16119543, 16185079, 16250614, 16250614, 16315893, 16315893, 16381428, 16381171, 16380915, 16446450, + 16446193, 16446192, 16445935, 16445679, 16511214, 16510957, 16510700, 16510443, 16510443, 16510186, 16509929, + 16509928, 16575207, 16574950, 16574950, 16574693, 16574436, 16574435, 16574178, 16573922, 16573921, 16573664, + 16638943, 16638686, 16638686, 16638429, 16638172, 16638171, 16637914, 16637658, 16637657, 16637400, 16637143, + 16637142, 16636886, 16702165, 16702164, 16701907, 16701650, 16701394, 16701393, 16701136, 16700879, 16700878, + 16700622, 16700365, 16700364, 16700107, 16699850, 16699850, 16699593, 16699336, 16699335, 16699078, 16698822, + 16698565, 16698564, 16698307, 16698051, 16698050, 16697793, 16697536, 16697535, 16762815, 16762558, 16762557, + 16762300, 16762043, 16761787, 16761786, 16761529, 16761272, 16761272, 16761015, 16695222, 16695221, 16694964, + 16694708, 16694707, 16694450, 16694193, 16693937, 16693936, 16693679, 16693422, 16693421, 16693165, 16692908, + 16692907, 16692650, 16692394, 16692393, 16692136, 16691879, 16691623, 16691622, 16691365, 16691108, 16691107, + 16690851, 16690594, 16690593, 16690336, 16624544, 16624287, 16624286, 16624029, 16623773, 16623772, 16623515, + 16623258, 16623258, 16623001, 16622744, 16622487, 16622487, 16622230, 16621973, 16556436, 16556180 + }; + public static final Colorcet CWR = new Colorcet(cwr, FAMILY.DIVERGENT); - private static final int[] dimgray = { - 1776411, 1842204, 1842204, 1907997, 1973790, 1973790, 2039583, 2105376, 2105376, 2171169, - 2236962, 2236962, 2302755, 2368548, 2368548, 2434341, 2500134, 2565927, 2565927, 2631720, - 2697513, 2697513, 2763306, 2829099, 2829099, 2894892, 2960685, 3026478, 3026478, 3092271, - 3158064, 3223857, 3223857, 3289650, 3355443, 3355443, 3421236, 3487029, 3552822, 3552822, - 3618615, 3684408, 3750201, 3750201, 3815994, 3881787, 3947580, 3947580, 4013373, 4079166, - 4144959, 4144959, 4210752, 4276545, 4342338, 4342338, 4408131, 4473924, 4539717, 4539717, - 4605510, 4671303, 4737096, 4737096, 4802889, 4868682, 4934475, 5000268, 5000268, 5066061, - 5131854, 5197647, 5197647, 5263440, 5329233, 5395026, 5460819, 5460819, 5526612, 5592405, - 5658198, 5723991, 5723991, 5789784, 5855577, 5921370, 5987163, 5987163, 6052956, 6118749, - 6184542, 6250335, 6250335, 6316128, 6381921, 6447714, 6513507, 6513507, 6579300, 6645093, - 6710886, 6776679, 6776679, 6842472, 6908265, 6974058, 7039851, 7105644, 7105644, 7171437, - 7237230, 7303023, 7368816, 7434609, 7434609, 7500402, 7566195, 7631988, 7697781, 7763574, - 7763574, 7829367, 7895160, 7960953, 8026746, 8092539, 8092539, 8158332, 8224125, 8289918, - 8355711, 8421504, 8487297, 8487297, 8553090, 8618883, 8684676, 8750469, 8816262, 8882055, - 8882055, 8947848, 9013641, 9079434, 9145227, 9211020, 9276813, 9276813, 9342606, 9408399, - 9474192, 9539985, 9605778, 9671571, 9737364, 9737364, 9803157, 9868950, 9934743, 10000536, - 10066329, 10132122, 10197915, 10197915, 10263708, 10329501, 10395294, 10461087, 10526880, - 10592673, 10658466, 10658723, 10724259, 10790052, 10855845, 10921638, 10987431, 11053224, - 11119017, 11184810, 11250603, 11250603, 11316396, 11382189, 11447982, 11513775, 11579568, - 11645361, 11711154, 11776947, 11842740, 11842740, 11908533, 11974326, 12040119, 12105912, - 12171705, 12237498, 12303291, 12369084, 12434877, 12500670, 12500670, 12566463, 12632256, - 12698049, 12763842, 12829635, 12895428, 12961221, 13027014, 13092807, 13158600, 13224393, - 13224650, 13290186, 13355979, 13421772, 13487565, 13553358, 13619151, 13684944, 13750737, - 13816530, 13882323, 13948116, 14013909, 14079702, 14145495, 14145495, 14211288, 14277081, - 14342874, 14408667, 14474460, 14540253, 14606046, 14671839, 14737632, 14803425, 14869218, - 14935011, 15000804, 15066597, 15132390, 15198183, 15198440, 15263976, 15329769, 15395562, - 15461355, 15527148, 15592941, 15658734, 15724527, 15790320 - }; - public static final Colorcet DIMGRAY = new Colorcet(dimgray, FAMILY.LINEAR); + private static final int[] dimgray = { + 1776411, 1842204, 1842204, 1907997, 1973790, 1973790, 2039583, 2105376, 2105376, 2171169, 2236962, 2236962, + 2302755, 2368548, 2368548, 2434341, 2500134, 2565927, 2565927, 2631720, 2697513, 2697513, 2763306, 2829099, + 2829099, 2894892, 2960685, 3026478, 3026478, 3092271, 3158064, 3223857, 3223857, 3289650, 3355443, 3355443, + 3421236, 3487029, 3552822, 3552822, 3618615, 3684408, 3750201, 3750201, 3815994, 3881787, 3947580, 3947580, + 4013373, 4079166, 4144959, 4144959, 4210752, 4276545, 4342338, 4342338, 4408131, 4473924, 4539717, 4539717, + 4605510, 4671303, 4737096, 4737096, 4802889, 4868682, 4934475, 5000268, 5000268, 5066061, 5131854, 5197647, + 5197647, 5263440, 5329233, 5395026, 5460819, 5460819, 5526612, 5592405, 5658198, 5723991, 5723991, 5789784, + 5855577, 5921370, 5987163, 5987163, 6052956, 6118749, 6184542, 6250335, 6250335, 6316128, 6381921, 6447714, + 6513507, 6513507, 6579300, 6645093, 6710886, 6776679, 6776679, 6842472, 6908265, 6974058, 7039851, 7105644, + 7105644, 7171437, 7237230, 7303023, 7368816, 7434609, 7434609, 7500402, 7566195, 7631988, 7697781, 7763574, + 7763574, 7829367, 7895160, 7960953, 8026746, 8092539, 8092539, 8158332, 8224125, 8289918, 8355711, 8421504, + 8487297, 8487297, 8553090, 8618883, 8684676, 8750469, 8816262, 8882055, 8882055, 8947848, 9013641, 9079434, + 9145227, 9211020, 9276813, 9276813, 9342606, 9408399, 9474192, 9539985, 9605778, 9671571, 9737364, 9737364, + 9803157, 9868950, 9934743, 10000536, 10066329, 10132122, 10197915, 10197915, 10263708, 10329501, 10395294, + 10461087, 10526880, 10592673, 10658466, 10658723, 10724259, 10790052, 10855845, 10921638, 10987431, 11053224, + 11119017, 11184810, 11250603, 11250603, 11316396, 11382189, 11447982, 11513775, 11579568, 11645361, 11711154, + 11776947, 11842740, 11842740, 11908533, 11974326, 12040119, 12105912, 12171705, 12237498, 12303291, 12369084, + 12434877, 12500670, 12500670, 12566463, 12632256, 12698049, 12763842, 12829635, 12895428, 12961221, 13027014, + 13092807, 13158600, 13224393, 13224650, 13290186, 13355979, 13421772, 13487565, 13553358, 13619151, 13684944, + 13750737, 13816530, 13882323, 13948116, 14013909, 14079702, 14145495, 14145495, 14211288, 14277081, 14342874, + 14408667, 14474460, 14540253, 14606046, 14671839, 14737632, 14803425, 14869218, 14935011, 15000804, 15066597, + 15132390, 15198183, 15198440, 15263976, 15329769, 15395562, 15461355, 15527148, 15592941, 15658734, 15724527, + 15790320 + }; + public static final Colorcet DIMGRAY = new Colorcet(dimgray, FAMILY.LINEAR); - private static final int[] fire = { - 0, 393216, 851968, 1179648, 1441792, 1638400, 1835008, 2031616, 2228224, 2359296, 2490368, - 2621440, 2818048, 2949120, 3014656, 3145728, 3276800, 3407872, 3473408, 3604480, 3670016, - 3801088, 3866624, 3997696, 4063232, 4194304, 4259840, 4390912, 4456448, 4587520, 4653056, - 4784128, 4849664, 4980736, 5046272, 5177344, 5242880, 5373952, 5439488, 5570560, 5636096, - 5767168, 5832960, 5964032, 6095104, 6160640, 6291712, 6357248, 6488320, 6619392, 6684928, - 6816000, 6881536, 7012608, 7143680, 7209216, 7340288, 7405824, 7536896, 7667968, 7733504, - 7864832, 7995904, 8061440, 8192512, 8323584, 8389120, 8520192, 8651264, 8716800, 8847872, - 8978944, 9044480, 9175808, 9306880, 9437952, 9503488, 9634560, 9765632, 9831168, 9962240, - 10093312, 10224384, 10290176, 10421248, 10552320, 10617856, 10748928, 10880000, 11011072, - 11076608, 11207936, 11339008, 11470080, 11535616, 11666688, 11797760, 11929088, 12060160, - 12125696, 12256768, 12387840, 12519168, 12584704, 12715776, 12846848, 12978176, 13109248, - 13174784, 13305856, 13437184, 13568256, 13699328, 13765120, 13896192, 14027264, 14158592, - 14289664, 14355456, 14486528, 14617856, 14748928, 14880256, 14946048, 15077120, 15208448, - 15339776, 15405824, 15537152, 15603200, 15734784, 15801088, 15867136, 15933440, 16065280, - 16131584, 16132352, 16198656, 16264960, 16331264, 16332032, 16398336, 16399104, 16465152, - 16465920, 16466688, 16532992, 16533760, 16534272, 16600576, 16601344, 16601856, 16602624, - 16603136, 16669440, 16669952, 16670464, 16671232, 16671744, 16672256, 16672768, 16673536, - 16674048, 16674560, 16675072, 16675584, 16676096, 16676608, 16677120, 16677888, 16743936, - 16744448, 16744960, 16745216, 16745728, 16746240, 16746752, 16747264, 16747776, 16748288, - 16748800, 16749312, 16749568, 16750080, 16750592, 16751104, 16751616, 16751872, 16752384, - 16752896, 16753408, 16753665, 16754177, 16754689, 16755201, 16755457, 16755969, 16756481, - 16756737, 16757250, 16757762, 16758018, 16758530, 16759042, 16759298, 16759811, 16760067, - 16760579, 16761091, 16761348, 16761860, 16762372, 16762628, 16763141, 16763397, 16763909, - 16764422, 16764678, 16765190, 16765447, 16765959, 16766216, 16766728, 16767241, 16767497, - 16768010, 16768266, 16768779, 16769035, 16769548, 16769805, 16770318, 16770575, 16771088, - 16771601, 16771858, 16772372, 16772631, 16773146, 16773406, 16773924, 16774186, 16774450, - 16774971, 16775239, 16775507, 16776034, 16776050, 16776323, 16776597, 16776872, 16776890, - 16776908, 16776926, 16776942, 16777215 - }; - public static final Colorcet FIRE = new Colorcet(fire, FAMILY.LINEAR); + private static final int[] fire = { + 0, 393216, 851968, 1179648, 1441792, 1638400, 1835008, 2031616, 2228224, 2359296, 2490368, 2621440, 2818048, + 2949120, 3014656, 3145728, 3276800, 3407872, 3473408, 3604480, 3670016, 3801088, 3866624, 3997696, 4063232, + 4194304, 4259840, 4390912, 4456448, 4587520, 4653056, 4784128, 4849664, 4980736, 5046272, 5177344, 5242880, + 5373952, 5439488, 5570560, 5636096, 5767168, 5832960, 5964032, 6095104, 6160640, 6291712, 6357248, 6488320, + 6619392, 6684928, 6816000, 6881536, 7012608, 7143680, 7209216, 7340288, 7405824, 7536896, 7667968, 7733504, + 7864832, 7995904, 8061440, 8192512, 8323584, 8389120, 8520192, 8651264, 8716800, 8847872, 8978944, 9044480, + 9175808, 9306880, 9437952, 9503488, 9634560, 9765632, 9831168, 9962240, 10093312, 10224384, 10290176, 10421248, + 10552320, 10617856, 10748928, 10880000, 11011072, 11076608, 11207936, 11339008, 11470080, 11535616, 11666688, + 11797760, 11929088, 12060160, 12125696, 12256768, 12387840, 12519168, 12584704, 12715776, 12846848, 12978176, + 13109248, 13174784, 13305856, 13437184, 13568256, 13699328, 13765120, 13896192, 14027264, 14158592, 14289664, + 14355456, 14486528, 14617856, 14748928, 14880256, 14946048, 15077120, 15208448, 15339776, 15405824, 15537152, + 15603200, 15734784, 15801088, 15867136, 15933440, 16065280, 16131584, 16132352, 16198656, 16264960, 16331264, + 16332032, 16398336, 16399104, 16465152, 16465920, 16466688, 16532992, 16533760, 16534272, 16600576, 16601344, + 16601856, 16602624, 16603136, 16669440, 16669952, 16670464, 16671232, 16671744, 16672256, 16672768, 16673536, + 16674048, 16674560, 16675072, 16675584, 16676096, 16676608, 16677120, 16677888, 16743936, 16744448, 16744960, + 16745216, 16745728, 16746240, 16746752, 16747264, 16747776, 16748288, 16748800, 16749312, 16749568, 16750080, + 16750592, 16751104, 16751616, 16751872, 16752384, 16752896, 16753408, 16753665, 16754177, 16754689, 16755201, + 16755457, 16755969, 16756481, 16756737, 16757250, 16757762, 16758018, 16758530, 16759042, 16759298, 16759811, + 16760067, 16760579, 16761091, 16761348, 16761860, 16762372, 16762628, 16763141, 16763397, 16763909, 16764422, + 16764678, 16765190, 16765447, 16765959, 16766216, 16766728, 16767241, 16767497, 16768010, 16768266, 16768779, + 16769035, 16769548, 16769805, 16770318, 16770575, 16771088, 16771601, 16771858, 16772372, 16772631, 16773146, + 16773406, 16773924, 16774186, 16774450, 16774971, 16775239, 16775507, 16776034, 16776050, 16776323, 16776597, + 16776872, 16776890, 16776908, 16776926, 16776942, 16777215 + }; + public static final Colorcet FIRE = new Colorcet(fire, FAMILY.LINEAR); - private static final int[] glasbey = { - 14024704, 9190399, 100096, 44230, 9961216, 16744145, 7012431, 16753967, 5716736, 22105, 221, - 64975, 10581353, 12367615, 9811319, 12518328, 6575220, 7929856, 488664, 16643216, 19200, - 9337088, 16740966, 15579320, 6127206, 10151167, 15401079, 10845112, 5832867, 247296, 10373888, - 10238799, 13288192, 7373463, 44937, 8554495, 6108731, 3670016, 16629759, 12445375, 14380289, - 9681077, 14963455, 3101314, 12805776, 5530143, 12885618, 230023, 6940288, 8398480, 7189503, - 5059583, 8758017, 16581578, 12690884, 12867142, 7690045, 91970, 54996, 14344191, 16383744, - 6907823, 12818176, 14798236, 14325247, 12190717, 9523842, 10485874, 5675604, 13864078, 3556390, - 9938371, 9342046, 16729600, 13172729, 11431423, 7262119, 12582796, 9196721, 7812632, 16752761, - 11010079, 16718916, 6164771, 6789011, 16735891, 4941684, 5411276, 11169841, 118781, 50027, - 6304861, 9491503, 12571772, 5260449, 5055244, 8149248, 16764228, 8520143, 5111295, 8978493, - 8082011, 29852, 11174551, 8417422, 6448381, 12792969, 13445190, 16751285, 12803514, 2189057, - 36452, 6455331, 9013183, 9952724, 13467223, 13743707, 6291566, 10048580, 11519707, 15925201, - 60161, 13469116, 4456644, 7969918, 7499846, 9699258, 21697, 11310059, 4170518, 6175360, 19251, - 8173779, 9906688, 3698276, 12058715, 16744509, 16765416, 8400729, 2176000, 10575214, 5223855, - 10395206, 3374141, 12665088, 13035581, 7013864, 7715919, 10863784, 14308462, 14192184, 16481535, - 4940873, 14074859, 7941430, 4951717, 4622335, 10682563, 15311828, 16759927, 4605952, 10602239, - 9478633, 5204371, 15097265, 10391727, 5722154, 11492820, 8744417, 12676722, 14942434, 12105354, - 3681536, 14843555, 11287343, 11057739, 6927746, 9687440, 11504710, 482935, 38793, 5836545, - 5995648, 3102502, 14967867, 6176552, 7489980, 4936299, 13138397, 10236304, 13166322, 371435, - 10972058, 15118080, 6356834, 15916288, 7816193, 6300737, 6782666, 7970479, 845985, 10287067, - 8585333, 9334089, 14827823, 12077419, 7948677, 16764853, 4939206, 14857105, 16731117, 14086053, - 12541990, 14066613, 12352512, 8875697, 16723873, 16771247, 3364683, 12094585, 7046994, 12358609, - 1763069, 10566514, 10703016, 7143575, 9004155, 5855114, 16354954, 15128188, 7367425, 1595903, - 1451775, 55382, 16228861, 7968059, 11642836, 8310749, 51887, 7947835, 14352358, 14353841, - 15916543, 10740846, 8983331, 6711170, 15269232, 14199528, 14662356, 16601705, 7712410, 9909215, - 14971518, 9197862, 7816809, 3095976 - }; - public static final Colorcet GLASBEY = new Colorcet(glasbey, FAMILY.CATEGORICAL); + private static final int[] glasbey = { + 14024704, 9190399, 100096, 44230, 9961216, 16744145, 7012431, 16753967, 5716736, 22105, 221, 64975, 10581353, + 12367615, 9811319, 12518328, 6575220, 7929856, 488664, 16643216, 19200, 9337088, 16740966, 15579320, 6127206, + 10151167, 15401079, 10845112, 5832867, 247296, 10373888, 10238799, 13288192, 7373463, 44937, 8554495, 6108731, + 3670016, 16629759, 12445375, 14380289, 9681077, 14963455, 3101314, 12805776, 5530143, 12885618, 230023, 6940288, + 8398480, 7189503, 5059583, 8758017, 16581578, 12690884, 12867142, 7690045, 91970, 54996, 14344191, 16383744, + 6907823, 12818176, 14798236, 14325247, 12190717, 9523842, 10485874, 5675604, 13864078, 3556390, 9938371, + 9342046, 16729600, 13172729, 11431423, 7262119, 12582796, 9196721, 7812632, 16752761, 11010079, 16718916, + 6164771, 6789011, 16735891, 4941684, 5411276, 11169841, 118781, 50027, 6304861, 9491503, 12571772, 5260449, + 5055244, 8149248, 16764228, 8520143, 5111295, 8978493, 8082011, 29852, 11174551, 8417422, 6448381, 12792969, + 13445190, 16751285, 12803514, 2189057, 36452, 6455331, 9013183, 9952724, 13467223, 13743707, 6291566, 10048580, + 11519707, 15925201, 60161, 13469116, 4456644, 7969918, 7499846, 9699258, 21697, 11310059, 4170518, 6175360, + 19251, 8173779, 9906688, 3698276, 12058715, 16744509, 16765416, 8400729, 2176000, 10575214, 5223855, 10395206, + 3374141, 12665088, 13035581, 7013864, 7715919, 10863784, 14308462, 14192184, 16481535, 4940873, 14074859, + 7941430, 4951717, 4622335, 10682563, 15311828, 16759927, 4605952, 10602239, 9478633, 5204371, 15097265, + 10391727, 5722154, 11492820, 8744417, 12676722, 14942434, 12105354, 3681536, 14843555, 11287343, 11057739, + 6927746, 9687440, 11504710, 482935, 38793, 5836545, 5995648, 3102502, 14967867, 6176552, 7489980, 4936299, + 13138397, 10236304, 13166322, 371435, 10972058, 15118080, 6356834, 15916288, 7816193, 6300737, 6782666, 7970479, + 845985, 10287067, 8585333, 9334089, 14827823, 12077419, 7948677, 16764853, 4939206, 14857105, 16731117, + 14086053, 12541990, 14066613, 12352512, 8875697, 16723873, 16771247, 3364683, 12094585, 7046994, 12358609, + 1763069, 10566514, 10703016, 7143575, 9004155, 5855114, 16354954, 15128188, 7367425, 1595903, 1451775, 55382, + 16228861, 7968059, 11642836, 8310749, 51887, 7947835, 14352358, 14353841, 15916543, 10740846, 8983331, 6711170, + 15269232, 14199528, 14662356, 16601705, 7712410, 9909215, 14971518, 9197862, 7816809, 3095976 + }; + public static final Colorcet GLASBEY = new Colorcet(glasbey, FAMILY.CATEGORICAL); - private static final int[] glasbey_dark = { - 14024704, 9190399, 100096, 44230, 15115520, 16744145, 7012431, 5716736, 22105, 1433996, 221, - 10581353, 12367615, 12518328, 6575218, 7929856, 488664, 7510652, 16742226, 19200, 9337601, - 15859835, 9353728, 10845112, 5833123, 14856111, 10500690, 10602696, 10373888, 5531460, 12239753, - 6192007, 6305851, 8554495, 3670016, 14832383, 3101314, 8309503, 12871310, 32873, 9543350, - 13399047, 8268430, 48547, 2994514, 5059583, 58368, 16711885, 13129544, 14982399, 1876479, - 7237802, 13146729, 7820859, 252646, 12690371, 16738697, 12189949, 9523840, 10355060, 9675087, - 3556388, 11496959, 5860608, 16724294, 8552534, 27949, 9000623, 5851555, 7812118, 8766362, - 6164771, 13927808, 10692632, 34737, 13238340, 16752726, 15420672, 7051008, 5408073, 7690496, - 13157183, 9556848, 4954003, 5055244, 6304859, 8585423, 9044017, 10382897, 11305881, 12988809, - 87096, 551811, 8890603, 6579951, 12803514, 106096, 8409177, 8547980, 11780058, 12095528, - 16750513, 10982369, 6917309, 4935425, 4719052, 6291566, 4483430, 10245698, 8105141, 13468604, - 21697, 8073039, 16481280, 3456768, 16751751, 14792297, 5398647, 5978748, 15574490, 15684259, - 6127209, 12810063, 13715559, 7209195, 2044928, 12665091, 7197889, 4616350, 10551747, 688777, - 11511041, 10836843, 16611327, 9078190, 13008616, 10136197, 8874968, 113398, 11492817, 5853226, - 11862110, 8172905, 4818431, 49538, 13735338, 10701736, 14812642, 1483520, 3681536, 8597299, - 6133162, 5836544, 8078848, 7237169, 3364646, 5071029, 10589540, 6438696, 4510807, 7383759, - 2976589, 7516062, 16585984, 14201745, 7964987, 8177368, 14389302, 15425629, 15425236, 14973863, - 10840983, 38724, 12213793, 12364882, 8902703, 8860786, 11446481, 14847074, 13742571, 3555998, - 3849665, 6724685, 10355609, 5066105, 8080261, 12792881, 9201271, 11141165, 8257909, 98893, - 7489895, 7501712, 7209113, 10533458, 14773809, 12872048, 7166869, 10697588, 3236352, 8847439, - 3364713, 12225660, 1595903, 9474305, 2853588, 1451775, 2216959, 10719407, 9071951, 6103357, - 14353331, 7231178, 6563873, 11302656, 10731510, 11895622, 9910491, 11620499, 7488163, 8883921, - 9007281, 7057206, 5863880, 13016831, 5669658, 54951, 8537656, 1131036, 5876341, 9460481, - 16139376, 16750339, 14762545, 12227023, 3430221, 16220284, 9450496, 11783424, 2989779, 7965342, - 5275772, 12662486, 15402322, 12102782, 4747313, 8623460, 14195849, 25763, 4952183, 9330839, - 16732728, 10961467, 28272, 9929533, 14397384 - }; - public static final Colorcet GLASBEY_DARK = new Colorcet(glasbey_dark, FAMILY.CATEGORICAL); + private static final int[] glasbey_dark = { + 14024704, 9190399, 100096, 44230, 15115520, 16744145, 7012431, 5716736, 22105, 1433996, 221, 10581353, 12367615, + 12518328, 6575218, 7929856, 488664, 7510652, 16742226, 19200, 9337601, 15859835, 9353728, 10845112, 5833123, + 14856111, 10500690, 10602696, 10373888, 5531460, 12239753, 6192007, 6305851, 8554495, 3670016, 14832383, + 3101314, 8309503, 12871310, 32873, 9543350, 13399047, 8268430, 48547, 2994514, 5059583, 58368, 16711885, + 13129544, 14982399, 1876479, 7237802, 13146729, 7820859, 252646, 12690371, 16738697, 12189949, 9523840, + 10355060, 9675087, 3556388, 11496959, 5860608, 16724294, 8552534, 27949, 9000623, 5851555, 7812118, 8766362, + 6164771, 13927808, 10692632, 34737, 13238340, 16752726, 15420672, 7051008, 5408073, 7690496, 13157183, 9556848, + 4954003, 5055244, 6304859, 8585423, 9044017, 10382897, 11305881, 12988809, 87096, 551811, 8890603, 6579951, + 12803514, 106096, 8409177, 8547980, 11780058, 12095528, 16750513, 10982369, 6917309, 4935425, 4719052, 6291566, + 4483430, 10245698, 8105141, 13468604, 21697, 8073039, 16481280, 3456768, 16751751, 14792297, 5398647, 5978748, + 15574490, 15684259, 6127209, 12810063, 13715559, 7209195, 2044928, 12665091, 7197889, 4616350, 10551747, 688777, + 11511041, 10836843, 16611327, 9078190, 13008616, 10136197, 8874968, 113398, 11492817, 5853226, 11862110, + 8172905, 4818431, 49538, 13735338, 10701736, 14812642, 1483520, 3681536, 8597299, 6133162, 5836544, 8078848, + 7237169, 3364646, 5071029, 10589540, 6438696, 4510807, 7383759, 2976589, 7516062, 16585984, 14201745, 7964987, + 8177368, 14389302, 15425629, 15425236, 14973863, 10840983, 38724, 12213793, 12364882, 8902703, 8860786, + 11446481, 14847074, 13742571, 3555998, 3849665, 6724685, 10355609, 5066105, 8080261, 12792881, 9201271, + 11141165, 8257909, 98893, 7489895, 7501712, 7209113, 10533458, 14773809, 12872048, 7166869, 10697588, 3236352, + 8847439, 3364713, 12225660, 1595903, 9474305, 2853588, 1451775, 2216959, 10719407, 9071951, 6103357, 14353331, + 7231178, 6563873, 11302656, 10731510, 11895622, 9910491, 11620499, 7488163, 8883921, 9007281, 7057206, 5863880, + 13016831, 5669658, 54951, 8537656, 1131036, 5876341, 9460481, 16139376, 16750339, 14762545, 12227023, 3430221, + 16220284, 9450496, 11783424, 2989779, 7965342, 5275772, 12662486, 15402322, 12102782, 4747313, 8623460, + 14195849, 25763, 4952183, 9330839, 16732728, 10961467, 28272, 9929533, 14397384 + }; + public static final Colorcet GLASBEY_DARK = new Colorcet(glasbey_dark, FAMILY.CATEGORICAL); - private static final int[] glasbey_light = { - 14024704, 100096, 11862271, 371910, 9961216, 16753967, 16748232, 7950942, 64975, 11511295, - 9677955, 10119424, 3565922, 13828236, 16643216, 13135462, 10412799, 51270, 11040684, 12106241, - 16039857, 16722173, 15912447, 40572, 16736768, 5661738, 9781023, 9449870, 16725092, 10544273, - 9214641, 8556582, 11405375, 7849658, 12357975, 14978815, 7518463, 13018561, 16748656, 13878140, - 12381659, 7046503, 9530966, 16383744, 12239327, 11294332, 16764163, 16730545, 12670467, 6130832, - 12666044, 30015, 12218109, 54419, 65397, 4825424, 13408144, 60397, 14384641, 16217481, 12096768, - 13124168, 53241, 7690022, 8770561, 15466452, 10976135, 14381768, 13296214, 9092957, 10559851, - 8739721, 9026255, 16759510, 11980714, 9912653, 6793728, 16638385, 16725544, 8419645, 14084351, - 10982854, 8299930, 13730467, 5538363, 15116402, 10289151, 14308480, 373674, 16755446, 13742063, - 14287197, 11278867, 6337413, 13910781, 11315801, 16489639, 11760187, 15886674, 11456980, - 10158019, 14398259, 15401411, 10027204, 13631390, 10836297, 3894529, 34169, 9802087, 9034675, - 7173120, 11165130, 519936, 8408893, 14188626, 16762978, 12058782, 10071261, 9457408, 9192560, - 5205586, 16746292, 13012685, 13951646, 11633261, 10287989, 5692791, 16318599, 10604031, 1297105, - 1150548, 13718693, 57283, 10715951, 7837531, 12233344, 7381935, 14089215, 15204666, 14173729, - 16745197, 11941986, 11980146, 9921131, 9008272, 41750, 62625, 12554482, 9037016, 10702229, - 7232768, 9225870, 9808426, 13005533, 11746049, 14064182, 14658742, 39584, 5869568, 9944232, - 11308200, 14341375, 5536882, 47721, 16761742, 12058836, 14667611, 6462075, 12578236, 12697085, - 8442845, 14845310, 16378701, 12545410, 13303631, 15692458, 15558655, 10045102, 7170370, - 14833248, 14509613, 10279773, 14851279, 12088576, 12976173, 14662874, 5879263, 16734682, - 3719585, 10381708, 11315912, 9789999, 11884130, 2850400, 11658240, 15574416, 9829858, 16733326, - 12414625, 11156278, 14208768, 11174093, 10518610, 14745832, 12802877, 11876997, 9205504, - 14400661, 5414547, 11517058, 9549238, 10966051, 16766191, 7974507, 6141259, 8453018, 4784111, - 9934152, 9668519, 3265536, 7268694, 11982059, 7361648, 15915914, 11195585, 8310258, 9026048, - 6600378, 16758016, 12812933, 13281886, 6584136, 5890815, 14634445, 15335289, 12347064, 12817829, - 6604404, 13735280, 7393103, 11169382, 10248357, 47104, 14850483, 12320875, 11790575, 13484004, - 7840578, 8741495, 5672539, 10399684, 15216544, 2391082, 8546083, 12565581, 14537637 - }; - public static final Colorcet GLASBEY_LIGHT = new Colorcet(glasbey_light, FAMILY.CATEGORICAL); + private static final int[] glasbey_light = { + 14024704, 100096, 11862271, 371910, 9961216, 16753967, 16748232, 7950942, 64975, 11511295, 9677955, 10119424, + 3565922, 13828236, 16643216, 13135462, 10412799, 51270, 11040684, 12106241, 16039857, 16722173, 15912447, 40572, + 16736768, 5661738, 9781023, 9449870, 16725092, 10544273, 9214641, 8556582, 11405375, 7849658, 12357975, + 14978815, 7518463, 13018561, 16748656, 13878140, 12381659, 7046503, 9530966, 16383744, 12239327, 11294332, + 16764163, 16730545, 12670467, 6130832, 12666044, 30015, 12218109, 54419, 65397, 4825424, 13408144, 60397, + 14384641, 16217481, 12096768, 13124168, 53241, 7690022, 8770561, 15466452, 10976135, 14381768, 13296214, + 9092957, 10559851, 8739721, 9026255, 16759510, 11980714, 9912653, 6793728, 16638385, 16725544, 8419645, + 14084351, 10982854, 8299930, 13730467, 5538363, 15116402, 10289151, 14308480, 373674, 16755446, 13742063, + 14287197, 11278867, 6337413, 13910781, 11315801, 16489639, 11760187, 15886674, 11456980, 10158019, 14398259, + 15401411, 10027204, 13631390, 10836297, 3894529, 34169, 9802087, 9034675, 7173120, 11165130, 519936, 8408893, + 14188626, 16762978, 12058782, 10071261, 9457408, 9192560, 5205586, 16746292, 13012685, 13951646, 11633261, + 10287989, 5692791, 16318599, 10604031, 1297105, 1150548, 13718693, 57283, 10715951, 7837531, 12233344, 7381935, + 14089215, 15204666, 14173729, 16745197, 11941986, 11980146, 9921131, 9008272, 41750, 62625, 12554482, 9037016, + 10702229, 7232768, 9225870, 9808426, 13005533, 11746049, 14064182, 14658742, 39584, 5869568, 9944232, 11308200, + 14341375, 5536882, 47721, 16761742, 12058836, 14667611, 6462075, 12578236, 12697085, 8442845, 14845310, + 16378701, 12545410, 13303631, 15692458, 15558655, 10045102, 7170370, 14833248, 14509613, 10279773, 14851279, + 12088576, 12976173, 14662874, 5879263, 16734682, 3719585, 10381708, 11315912, 9789999, 11884130, 2850400, + 11658240, 15574416, 9829858, 16733326, 12414625, 11156278, 14208768, 11174093, 10518610, 14745832, 12802877, + 11876997, 9205504, 14400661, 5414547, 11517058, 9549238, 10966051, 16766191, 7974507, 6141259, 8453018, 4784111, + 9934152, 9668519, 3265536, 7268694, 11982059, 7361648, 15915914, 11195585, 8310258, 9026048, 6600378, 16758016, + 12812933, 13281886, 6584136, 5890815, 14634445, 15335289, 12347064, 12817829, 6604404, 13735280, 7393103, + 11169382, 10248357, 47104, 14850483, 12320875, 11790575, 13484004, 7840578, 8741495, 5672539, 10399684, + 15216544, 2391082, 8546083, 12565581, 14537637 + }; + public static final Colorcet GLASBEY_LIGHT = new Colorcet(glasbey_light, FAMILY.CATEGORICAL); - private static final int[] gray = { - 0, 65793, 131586, 263172, 328965, 460551, 526344, 657930, 723723, 789516, 855309, 921102, - 1052688, 1118481, 1184274, 1184530, 1250067, 1315860, 1381653, 1447446, 1513239, 1513239, - 1579032, 1644825, 1710618, 1776411, 1776411, 1842204, 1907997, 1973790, 2039583, 2039583, - 2105376, 2171169, 2236962, 2302755, 2302755, 2368548, 2434341, 2500134, 2565927, 2565927, - 2631720, 2697513, 2763306, 2829099, 2894892, 2894892, 2960685, 3026478, 3092271, 3158064, - 3223857, 3223857, 3289650, 3355443, 3421236, 3487029, 3552822, 3618615, 3684408, 3684408, - 3750201, 3815994, 3881787, 3947580, 4013373, 4079166, 4144959, 4144959, 4210752, 4276545, - 4342338, 4408131, 4473924, 4539717, 4605510, 4671303, 4671303, 4737096, 4802889, 4868682, - 4934475, 5000268, 5066061, 5131854, 5197647, 5263440, 5329233, 5395026, 5395026, 5460819, - 5526612, 5592405, 5658198, 5723991, 5789784, 5855577, 5921370, 5987163, 6052956, 6118749, - 6184542, 6250335, 6316128, 6381921, 6447714, 6447714, 6513507, 6579300, 6645093, 6710886, - 6776679, 6842472, 6908265, 6974058, 7039851, 7105644, 7171437, 7237230, 7303023, 7368816, - 7434609, 7500402, 7566195, 7631988, 7697781, 7763574, 7829367, 7895160, 7960953, 8026746, - 8092539, 8158332, 8224125, 8289918, 8355711, 8421504, 8487297, 8553090, 8618883, 8684676, - 8750469, 8816262, 8882055, 8947848, 9013641, 9079434, 9145227, 9211020, 9276813, 9342606, - 9408399, 9474192, 9539985, 9605778, 9671571, 9737364, 9803157, 9868950, 9934743, 10000536, - 10066329, 10132122, 10197915, 10263708, 10329501, 10395551, 10526880, 10592673, 10658466, - 10724259, 10790052, 10855845, 10921638, 10987431, 11053224, 11119017, 11184810, 11250603, - 11316396, 11382189, 11447982, 11513775, 11579568, 11645361, 11711410, 11842740, 11908533, - 11974326, 12040119, 12105912, 12171705, 12237498, 12303291, 12369084, 12434877, 12500670, - 12566463, 12632256, 12698306, 12829635, 12895428, 12961221, 13027014, 13092807, 13158600, - 13224393, 13290186, 13355979, 13421772, 13487565, 13619151, 13684944, 13750737, 13816530, - 13882323, 13948116, 14013909, 14079702, 14145495, 14211288, 14277338, 14408667, 14474460, - 14540253, 14606046, 14671839, 14737632, 14803425, 14869218, 14935268, 15066597, 15132390, - 15198183, 15263976, 15329769, 15395562, 15461355, 15527148, 15658734, 15724527, 15790320, - 15856113, 15921906, 15987699, 16053492, 16119285, 16250871, 16316664, 16382457, 16448250, - 16514043, 16579836, 16645629, 16711679 - }; - public static final Colorcet GRAY = new Colorcet(gray, FAMILY.LINEAR); + private static final int[] gray = { + 0, 65793, 131586, 263172, 328965, 460551, 526344, 657930, 723723, 789516, 855309, 921102, 1052688, 1118481, + 1184274, 1184530, 1250067, 1315860, 1381653, 1447446, 1513239, 1513239, 1579032, 1644825, 1710618, 1776411, + 1776411, 1842204, 1907997, 1973790, 2039583, 2039583, 2105376, 2171169, 2236962, 2302755, 2302755, 2368548, + 2434341, 2500134, 2565927, 2565927, 2631720, 2697513, 2763306, 2829099, 2894892, 2894892, 2960685, 3026478, + 3092271, 3158064, 3223857, 3223857, 3289650, 3355443, 3421236, 3487029, 3552822, 3618615, 3684408, 3684408, + 3750201, 3815994, 3881787, 3947580, 4013373, 4079166, 4144959, 4144959, 4210752, 4276545, 4342338, 4408131, + 4473924, 4539717, 4605510, 4671303, 4671303, 4737096, 4802889, 4868682, 4934475, 5000268, 5066061, 5131854, + 5197647, 5263440, 5329233, 5395026, 5395026, 5460819, 5526612, 5592405, 5658198, 5723991, 5789784, 5855577, + 5921370, 5987163, 6052956, 6118749, 6184542, 6250335, 6316128, 6381921, 6447714, 6447714, 6513507, 6579300, + 6645093, 6710886, 6776679, 6842472, 6908265, 6974058, 7039851, 7105644, 7171437, 7237230, 7303023, 7368816, + 7434609, 7500402, 7566195, 7631988, 7697781, 7763574, 7829367, 7895160, 7960953, 8026746, 8092539, 8158332, + 8224125, 8289918, 8355711, 8421504, 8487297, 8553090, 8618883, 8684676, 8750469, 8816262, 8882055, 8947848, + 9013641, 9079434, 9145227, 9211020, 9276813, 9342606, 9408399, 9474192, 9539985, 9605778, 9671571, 9737364, + 9803157, 9868950, 9934743, 10000536, 10066329, 10132122, 10197915, 10263708, 10329501, 10395551, 10526880, + 10592673, 10658466, 10724259, 10790052, 10855845, 10921638, 10987431, 11053224, 11119017, 11184810, 11250603, + 11316396, 11382189, 11447982, 11513775, 11579568, 11645361, 11711410, 11842740, 11908533, 11974326, 12040119, + 12105912, 12171705, 12237498, 12303291, 12369084, 12434877, 12500670, 12566463, 12632256, 12698306, 12829635, + 12895428, 12961221, 13027014, 13092807, 13158600, 13224393, 13290186, 13355979, 13421772, 13487565, 13619151, + 13684944, 13750737, 13816530, 13882323, 13948116, 14013909, 14079702, 14145495, 14211288, 14277338, 14408667, + 14474460, 14540253, 14606046, 14671839, 14737632, 14803425, 14869218, 14935268, 15066597, 15132390, 15198183, + 15263976, 15329769, 15395562, 15461355, 15527148, 15658734, 15724527, 15790320, 15856113, 15921906, 15987699, + 16053492, 16119285, 16250871, 16316664, 16382457, 16448250, 16514043, 16579836, 16645629, 16711679 + }; + public static final Colorcet GRAY = new Colorcet(gray, FAMILY.LINEAR); - private static final int[] gwv = { - 3708430, 3839761, 3971092, 4102167, 4233498, 4364828, 4496159, 4627233, 4693027, 4824357, - 4955432, 5086762, 5152556, 5283886, 5414960, 5480754, 5612083, 5743157, 5808951, 5940281, - 6071611, 6137149, 6268478, 6334272, 6465346, 6531140, 6662469, 6728007, 6859337, 6925130, - 7056460, 7121998, 7253327, 7319121, 7450195, 7515988, 7647318, 7713112, 7844185, 7909979, - 8041309, 8106846, 8172640, 8303970, 8369507, 8500837, 8566631, 8697960, 8763498, 8829292, - 8960621, 9026159, 9091952, 9223282, 9289076, 9420149, 9485943, 9551737, 9682810, 9748604, - 9879934, 9945471, 10011265, 10142594, 10208388, 10273926, 10405255, 10471049, 10536587, - 10667916, 10733710, 10799248, 10930577, 10996371, 11061909, 11193238, 11259032, 11324825, - 11455899, 11521693, 11587486, 11718560, 11784354, 11850147, 11981221, 12047015, 12112808, - 12244138, 12309676, 12375469, 12506799, 12572337, 12638130, 12769460, 12834998, 12900791, - 13032121, 13097659, 13163452, 13294782, 13360576, 13426113, 13557443, 13623237, 13688774, - 13754568, 13885898, 13951436, 14017229, 14148559, 14214097, 14279890, 14411220, 14476758, - 14542551, 14608345, 14739419, 14805212, 14870750, 14936544, 15002081, 15133411, 15198948, - 15264485, 15264487, 15330280, 15395817, 15461098, 15461099, 15526636, 15526380, 15526381, - 15526126, 15525870, 15525870, 15525615, 15525359, 15525103, 15524847, 15524591, 15458799, - 15458543, 15458031, 15392239, 15391983, 15391727, 15325935, 15325679, 15325423, 15259375, - 15259119, 15258863, 15193070, 15192814, 15192558, 15126766, 15126254, 15060462, 15060206, - 15059950, 14994158, 14993902, 14993646, 14927598, 14927341, 14861549, 14861293, 14861037, - 14795245, 14794989, 14728941, 14728685, 14728429, 14662637, 14662381, 14596589, 14596332, - 14530284, 14530028, 14529772, 14463980, 14463724, 14397932, 14397420, 14397164, 14331372, - 14331116, 14265323, 14265067, 14199275, 14198763, 14132971, 14132715, 14132459, 14066667, - 14066411, 14000363, 14000106, 13934314, 13934058, 13868266, 13868010, 13867754, 13801706, - 13801450, 13735658, 13735402, 13669609, 13669353, 13603305, 13603049, 13537257, 13537001, - 13471209, 13470697, 13404905, 13404648, 13338856, 13338600, 13272808, 13272296, 13206504, - 13206248, 13140456, 13140200, 13074151, 13073895, 13008103, 13007847, 12942055, 12941543, - 12875751, 12875495, 12809702, 12809190, 12743398, 12743142, 12677350, 12677094, 12611046, - 12545254, 12544998, 12479205, 12478693, 12412901, 12412645, 12346597, 12346341, 12280549, - 12214756, 12214244, 12148452, 12148196, 12082148, 12081892, 12016100 - }; - public static final Colorcet GWV = new Colorcet(gwv, FAMILY.DIVERGENT); + private static final int[] gwv = { + 3708430, 3839761, 3971092, 4102167, 4233498, 4364828, 4496159, 4627233, 4693027, 4824357, 4955432, 5086762, + 5152556, 5283886, 5414960, 5480754, 5612083, 5743157, 5808951, 5940281, 6071611, 6137149, 6268478, 6334272, + 6465346, 6531140, 6662469, 6728007, 6859337, 6925130, 7056460, 7121998, 7253327, 7319121, 7450195, 7515988, + 7647318, 7713112, 7844185, 7909979, 8041309, 8106846, 8172640, 8303970, 8369507, 8500837, 8566631, 8697960, + 8763498, 8829292, 8960621, 9026159, 9091952, 9223282, 9289076, 9420149, 9485943, 9551737, 9682810, 9748604, + 9879934, 9945471, 10011265, 10142594, 10208388, 10273926, 10405255, 10471049, 10536587, 10667916, 10733710, + 10799248, 10930577, 10996371, 11061909, 11193238, 11259032, 11324825, 11455899, 11521693, 11587486, 11718560, + 11784354, 11850147, 11981221, 12047015, 12112808, 12244138, 12309676, 12375469, 12506799, 12572337, 12638130, + 12769460, 12834998, 12900791, 13032121, 13097659, 13163452, 13294782, 13360576, 13426113, 13557443, 13623237, + 13688774, 13754568, 13885898, 13951436, 14017229, 14148559, 14214097, 14279890, 14411220, 14476758, 14542551, + 14608345, 14739419, 14805212, 14870750, 14936544, 15002081, 15133411, 15198948, 15264485, 15264487, 15330280, + 15395817, 15461098, 15461099, 15526636, 15526380, 15526381, 15526126, 15525870, 15525870, 15525615, 15525359, + 15525103, 15524847, 15524591, 15458799, 15458543, 15458031, 15392239, 15391983, 15391727, 15325935, 15325679, + 15325423, 15259375, 15259119, 15258863, 15193070, 15192814, 15192558, 15126766, 15126254, 15060462, 15060206, + 15059950, 14994158, 14993902, 14993646, 14927598, 14927341, 14861549, 14861293, 14861037, 14795245, 14794989, + 14728941, 14728685, 14728429, 14662637, 14662381, 14596589, 14596332, 14530284, 14530028, 14529772, 14463980, + 14463724, 14397932, 14397420, 14397164, 14331372, 14331116, 14265323, 14265067, 14199275, 14198763, 14132971, + 14132715, 14132459, 14066667, 14066411, 14000363, 14000106, 13934314, 13934058, 13868266, 13868010, 13867754, + 13801706, 13801450, 13735658, 13735402, 13669609, 13669353, 13603305, 13603049, 13537257, 13537001, 13471209, + 13470697, 13404905, 13404648, 13338856, 13338600, 13272808, 13272296, 13206504, 13206248, 13140456, 13140200, + 13074151, 13073895, 13008103, 13007847, 12942055, 12941543, 12875751, 12875495, 12809702, 12809190, 12743398, + 12743142, 12677350, 12677094, 12611046, 12545254, 12544998, 12479205, 12478693, 12412901, 12412645, 12346597, + 12346341, 12280549, 12214756, 12214244, 12148452, 12148196, 12082148, 12081892, 12016100 + }; + public static final Colorcet GWV = new Colorcet(gwv, FAMILY.DIVERGENT); - private static final int[] isolum = { - 7328255, 7328255, 7328255, 7328255, 7328255, 7328254, 7394045, 7394044, 7394044, 7394043, - 7394042, 7394041, 7394040, 7394040, 7394039, 7394038, 7394037, 7394292, 7459828, 7459827, - 7459826, 7459825, 7459824, 7459824, 7459823, 7459822, 7459821, 7525356, 7525356, 7525355, - 7525610, 7525609, 7525608, 7525607, 7525607, 7591142, 7591141, 7591140, 7591139, 7591139, - 7591138, 7591137, 7656672, 7656671, 7656926, 7656926, 7656925, 7722460, 7722459, 7722458, - 7722457, 7722456, 7722456, 7787991, 7787990, 7787989, 7787988, 7853523, 7853522, 7853522, - 7853777, 7853776, 7919311, 7919310, 7919309, 7919308, 7984844, 7984843, 7984842, 8050377, - 8050376, 8050375, 8115910, 8115909, 8115909, 8115908, 8181443, 8181442, 8246977, 8246976, - 8246975, 8312510, 8312509, 8312508, 8378044, 8378043, 8443578, 8443577, 8443576, 8509111, - 8509110, 8574645, 8574644, 8640179, 8640178, 8705714, 8705713, 8771248, 8771247, 8836782, - 8902317, 8902316, 8967851, 8967850, 9033385, 9098920, 9098919, 9164454, 9229989, 9229989, - 9295524, 9361059, 9361058, 9426593, 9492128, 9557663, 9557406, 9622941, 9688476, 9754011, - 9819547, 9819546, 9885081, 9950616, 10016151, 10081686, 10146965, 10212501, 10278036, 10343571, - 10409106, 10474641, 10540177, 10605456, 10605455, 10670990, 10736526, 10802061, 10867596, - 10932876, 10998411, 11063946, 11129482, 11195017, 11260553, 11391368, 11456903, 11522439, - 11587974, 11653254, 11718789, 11784325, 11849860, 11915396, 11980675, 12046211, 12111746, - 12177282, 12177281, 12242561, 12308096, 12373632, 12439167, 12504447, 12569983, 12635518, - 12701054, 12766333, 12831869, 12897405, 12962940, 13028220, 13093755, 13159291, 13224827, - 13290106, 13355642, 13421178, 13421177, 13486457, 13551993, 13617529, 13683064, 13748344, - 13813880, 13879416, 13944695, 14010231, 14010231, 14075767, 14141047, 14206582, 14272118, - 14337654, 14402934, 14468470, 14468470, 14533750, 14599286, 14664821, 14730101, 14795637, - 14861173, 14861173, 14926453, 14991989, 15057525, 15122805, 15188341, 15188341, 15253621, - 15319157, 15384693, 15449973, 15449974, 15515510, 15581046, 15646326, 15711862, 15711862, - 15777142, 15842678, 15908215, 15907959, 15973495, 16039031, 16104311, 16104312, 16169848, - 16235128, 16300664, 16300665, 16365945, 16431481, 16431481, 16496762, 16562298, 16562298, - 16627579, 16693115, 16693115, 16758396, 16758396, 16758396, 16758141, 16758141, 16758141, - 16757886, 16757886, 16757886, 16757631, 16757631, 16757632, 16757376, 16757376, 16757377 - }; - public static final Colorcet ISOLUM = new Colorcet(isolum, FAMILY.LINEAR); + private static final int[] isolum = { + 7328255, 7328255, 7328255, 7328255, 7328255, 7328254, 7394045, 7394044, 7394044, 7394043, 7394042, 7394041, + 7394040, 7394040, 7394039, 7394038, 7394037, 7394292, 7459828, 7459827, 7459826, 7459825, 7459824, 7459824, + 7459823, 7459822, 7459821, 7525356, 7525356, 7525355, 7525610, 7525609, 7525608, 7525607, 7525607, 7591142, + 7591141, 7591140, 7591139, 7591139, 7591138, 7591137, 7656672, 7656671, 7656926, 7656926, 7656925, 7722460, + 7722459, 7722458, 7722457, 7722456, 7722456, 7787991, 7787990, 7787989, 7787988, 7853523, 7853522, 7853522, + 7853777, 7853776, 7919311, 7919310, 7919309, 7919308, 7984844, 7984843, 7984842, 8050377, 8050376, 8050375, + 8115910, 8115909, 8115909, 8115908, 8181443, 8181442, 8246977, 8246976, 8246975, 8312510, 8312509, 8312508, + 8378044, 8378043, 8443578, 8443577, 8443576, 8509111, 8509110, 8574645, 8574644, 8640179, 8640178, 8705714, + 8705713, 8771248, 8771247, 8836782, 8902317, 8902316, 8967851, 8967850, 9033385, 9098920, 9098919, 9164454, + 9229989, 9229989, 9295524, 9361059, 9361058, 9426593, 9492128, 9557663, 9557406, 9622941, 9688476, 9754011, + 9819547, 9819546, 9885081, 9950616, 10016151, 10081686, 10146965, 10212501, 10278036, 10343571, 10409106, + 10474641, 10540177, 10605456, 10605455, 10670990, 10736526, 10802061, 10867596, 10932876, 10998411, 11063946, + 11129482, 11195017, 11260553, 11391368, 11456903, 11522439, 11587974, 11653254, 11718789, 11784325, 11849860, + 11915396, 11980675, 12046211, 12111746, 12177282, 12177281, 12242561, 12308096, 12373632, 12439167, 12504447, + 12569983, 12635518, 12701054, 12766333, 12831869, 12897405, 12962940, 13028220, 13093755, 13159291, 13224827, + 13290106, 13355642, 13421178, 13421177, 13486457, 13551993, 13617529, 13683064, 13748344, 13813880, 13879416, + 13944695, 14010231, 14010231, 14075767, 14141047, 14206582, 14272118, 14337654, 14402934, 14468470, 14468470, + 14533750, 14599286, 14664821, 14730101, 14795637, 14861173, 14861173, 14926453, 14991989, 15057525, 15122805, + 15188341, 15188341, 15253621, 15319157, 15384693, 15449973, 15449974, 15515510, 15581046, 15646326, 15711862, + 15711862, 15777142, 15842678, 15908215, 15907959, 15973495, 16039031, 16104311, 16104312, 16169848, 16235128, + 16300664, 16300665, 16365945, 16431481, 16431481, 16496762, 16562298, 16562298, 16627579, 16693115, 16693115, + 16758396, 16758396, 16758396, 16758141, 16758141, 16758141, 16757886, 16757886, 16757886, 16757631, 16757631, + 16757632, 16757376, 16757376, 16757377 + }; + public static final Colorcet ISOLUM = new Colorcet(isolum, FAMILY.LINEAR); - private static final int[] kb = { - 0, 1, 259, 261, 519, 777, 66315, 66573, 66575, 66832, 67090, 132628, 132885, 132887, 133145, - 133146, 133404, 133405, 199199, 199200, 199457, 199459, 199716, 199717, 199719, 265512, 265513, - 265770, 265771, 265772, 266029, 266030, 266032, 266289, 331826, 331826, 332083, 332084, 332085, - 332342, 332343, 332344, 332345, 332602, 332603, 332603, 398396, 398397, 398398, 398399, 398656, - 398656, 398657, 398658, 398915, 398916, 398917, 399173, 464710, 464711, 464712, 464969, 464970, - 464970, 465227, 465228, 465229, 465230, 465487, 531024, 531024, 531025, 531282, 531283, 531284, - 531541, 531542, 531542, 531543, 531800, 531801, 597338, 597595, 597596, 597596, 597597, 597854, - 597855, 597856, 598113, 598114, 598115, 598115, 663908, 663909, 663910, 664167, 664168, 664169, - 664426, 664427, 664427, 664428, 664685, 730222, 730223, 730480, 730481, 730482, 730483, 730739, - 730740, 730741, 730998, 730999, 796536, 796793, 796794, 796795, 796796, 797052, 797053, 797054, - 797311, 797312, 797313, 863106, 863107, 863108, 863109, 863366, 863366, 863367, 863624, 863625, - 863626, 863883, 929420, 929421, 929422, 929679, 929680, 929681, 929937, 929938, 929939, 930196, - 930197, 995734, 995991, 995992, 995993, 995994, 996251, 996252, 996253, 996510, 996511, 996511, - 1062304, 1062305, 1062306, 1062563, 1062564, 1062565, 1062566, 1062823, 1062824, 1062825, - 1128618, 1128619, 1128620, 1128877, 1128878, 1128879, 1129136, 1129137, 1129137, 1129394, - 1129395, 1194932, 1195189, 1195190, 1195191, 1195192, 1195449, 1195450, 1195451, 1195708, - 1195709, 1261246, 1261503, 1261504, 1261505, 1261762, 1261763, 1261764, 1262021, 1262022, - 1262023, 1327816, 1327817, 1327818, 1328075, 1328076, 1328077, 1328334, 1328335, 1328336, - 1328337, 1328593, 1394130, 1394131, 1394388, 1394389, 1394390, 1394647, 1394648, 1394649, - 1394906, 1394907, 1460444, 1460701, 1460702, 1460703, 1460960, 1460961, 1460962, 1461219, - 1461220, 1461221, 1527014, 1527015, 1527016, 1527273, 1527274, 1527275, 1527532, 1527533, - 1527534, 1527791, 1593328, 1593329, 1593586, 1593587, 1593588, 1593845, 1593846, 1593847, - 1594104, 1594105, 1659642, 1659899, 1659900, 1659901, 1660159 - }; - public static final Colorcet KB = new Colorcet(kb, FAMILY.LINEAR); + private static final int[] kb = { + 0, 1, 259, 261, 519, 777, 66315, 66573, 66575, 66832, 67090, 132628, 132885, 132887, 133145, 133146, 133404, + 133405, 199199, 199200, 199457, 199459, 199716, 199717, 199719, 265512, 265513, 265770, 265771, 265772, 266029, + 266030, 266032, 266289, 331826, 331826, 332083, 332084, 332085, 332342, 332343, 332344, 332345, 332602, 332603, + 332603, 398396, 398397, 398398, 398399, 398656, 398656, 398657, 398658, 398915, 398916, 398917, 399173, 464710, + 464711, 464712, 464969, 464970, 464970, 465227, 465228, 465229, 465230, 465487, 531024, 531024, 531025, 531282, + 531283, 531284, 531541, 531542, 531542, 531543, 531800, 531801, 597338, 597595, 597596, 597596, 597597, 597854, + 597855, 597856, 598113, 598114, 598115, 598115, 663908, 663909, 663910, 664167, 664168, 664169, 664426, 664427, + 664427, 664428, 664685, 730222, 730223, 730480, 730481, 730482, 730483, 730739, 730740, 730741, 730998, 730999, + 796536, 796793, 796794, 796795, 796796, 797052, 797053, 797054, 797311, 797312, 797313, 863106, 863107, 863108, + 863109, 863366, 863366, 863367, 863624, 863625, 863626, 863883, 929420, 929421, 929422, 929679, 929680, 929681, + 929937, 929938, 929939, 930196, 930197, 995734, 995991, 995992, 995993, 995994, 996251, 996252, 996253, 996510, + 996511, 996511, 1062304, 1062305, 1062306, 1062563, 1062564, 1062565, 1062566, 1062823, 1062824, 1062825, + 1128618, 1128619, 1128620, 1128877, 1128878, 1128879, 1129136, 1129137, 1129137, 1129394, 1129395, 1194932, + 1195189, 1195190, 1195191, 1195192, 1195449, 1195450, 1195451, 1195708, 1195709, 1261246, 1261503, 1261504, + 1261505, 1261762, 1261763, 1261764, 1262021, 1262022, 1262023, 1327816, 1327817, 1327818, 1328075, 1328076, + 1328077, 1328334, 1328335, 1328336, 1328337, 1328593, 1394130, 1394131, 1394388, 1394389, 1394390, 1394647, + 1394648, 1394649, 1394906, 1394907, 1460444, 1460701, 1460702, 1460703, 1460960, 1460961, 1460962, 1461219, + 1461220, 1461221, 1527014, 1527015, 1527016, 1527273, 1527274, 1527275, 1527532, 1527533, 1527534, 1527791, + 1593328, 1593329, 1593586, 1593587, 1593588, 1593845, 1593846, 1593847, 1594104, 1594105, 1659642, 1659899, + 1659900, 1659901, 1660159 + }; + public static final Colorcet KB = new Colorcet(kb, FAMILY.LINEAR); - private static final int[] kbc = { - 78, 336, 338, 340, 597, 599, 601, 603, 605, 607, 66145, 66147, 131685, 197223, 197225, 262764, - 328302, 393840, 393842, 459380, 524918, 590200, 590202, 655740, 721278, 721281, 786819, 852357, - 852359, 917897, 917899, 983438, 983440, 983442, 1048980, 1048983, 1048985, 1048987, 1048989, - 1048992, 983458, 983716, 918183, 918185, 852651, 787374, 721840, 656562, 656565, 591031, 591289, - 526011, 526013, 460735, 460738, 460996, 461254, 461512, 461770, 527564, 527822, 593360, 659153, - 724947, 790741, 856535, 922329, 988123, 1053916, 1185246, 1251040, 1316833, 1448163, 1513957, - 1579750, 1711079, 1776873, 1842922, 1908715, 1974509, 2040558, 2106351, 2172144, 2238193, - 2303986, 2370035, 2435828, 2436341, 2502389, 2568182, 2634231, 2634487, 2700536, 2766584, - 2766841, 2832889, 2833401, 2899450, 2899706, 2965754, 2966266, 2966522, 2967034, 3033082, - 3033339, 3033851, 3034363, 3100155, 3100667, 3100923, 3101435, 3101691, 3102203, 3102715, - 3102971, 3103484, 3103740, 3104252, 3104508, 3104764, 3105276, 3105532, 3040508, 3040764, - 3041276, 3041532, 2976508, 2976765, 2977021, 2911997, 2912253, 2912765, 2913021, 2847741, - 2848253, 2848509, 2848765, 2849277, 2849533, 2849789, 2850301, 2850557, 2850813, 2851325, - 2917117, 2917373, 2917629, 2918141, 2983933, 2984189, 3050237, 3050493, 3116285, 3116541, - 3182589, 3182845, 3248637, 3314429, 3314685, 3380733, 3380989, 3446781, 3447037, 3447549, - 3513340, 3513596, 3513852, 3514364, 3514620, 3514876, 3515388, 3515644, 3515900, 3516156, - 3516668, 3516924, 3451644, 3451900, 3452412, 3387132, 3387388, 3322108, 3322620, 3257340, - 3192060, 3127036, 3127292, 3062012, 2996732, 2997244, 2931964, 2866684, 2867195, 2801915, - 2802171, 2736891, 2737403, 2737659, 2737915, 2672635, 2672891, 2673403, 2673659, 2673915, - 2674171, 2740219, 2740475, 2740730, 2806522, 2806778, 2872826, 2873082, 2938874, 3004666, - 3070458, 3202042, 3333369, 3530233, 3727097, 3923961, 4120825, 4383225, 4580089, 4842489, - 5039353, 5301753, 5498616, 5761016, 5957880, 6220280, 6417144, 6679544, 6876408, 7138552, - 7335416, 7597815, 7794679, 7991543, 8253943, 8450807, 8647415, 8909815, 9106679, 9303542, - 9500406, 9697270, 9959414, 10156278, 10353142, 10550006, 10746614, 10943478, 11140341, 11402741, - 11599605, 11796213 - }; - public static final Colorcet KBC = new Colorcet(kbc, FAMILY.LINEAR); + private static final int[] kbc = { + 78, 336, 338, 340, 597, 599, 601, 603, 605, 607, 66145, 66147, 131685, 197223, 197225, 262764, 328302, 393840, + 393842, 459380, 524918, 590200, 590202, 655740, 721278, 721281, 786819, 852357, 852359, 917897, 917899, 983438, + 983440, 983442, 1048980, 1048983, 1048985, 1048987, 1048989, 1048992, 983458, 983716, 918183, 918185, 852651, + 787374, 721840, 656562, 656565, 591031, 591289, 526011, 526013, 460735, 460738, 460996, 461254, 461512, 461770, + 527564, 527822, 593360, 659153, 724947, 790741, 856535, 922329, 988123, 1053916, 1185246, 1251040, 1316833, + 1448163, 1513957, 1579750, 1711079, 1776873, 1842922, 1908715, 1974509, 2040558, 2106351, 2172144, 2238193, + 2303986, 2370035, 2435828, 2436341, 2502389, 2568182, 2634231, 2634487, 2700536, 2766584, 2766841, 2832889, + 2833401, 2899450, 2899706, 2965754, 2966266, 2966522, 2967034, 3033082, 3033339, 3033851, 3034363, 3100155, + 3100667, 3100923, 3101435, 3101691, 3102203, 3102715, 3102971, 3103484, 3103740, 3104252, 3104508, 3104764, + 3105276, 3105532, 3040508, 3040764, 3041276, 3041532, 2976508, 2976765, 2977021, 2911997, 2912253, 2912765, + 2913021, 2847741, 2848253, 2848509, 2848765, 2849277, 2849533, 2849789, 2850301, 2850557, 2850813, 2851325, + 2917117, 2917373, 2917629, 2918141, 2983933, 2984189, 3050237, 3050493, 3116285, 3116541, 3182589, 3182845, + 3248637, 3314429, 3314685, 3380733, 3380989, 3446781, 3447037, 3447549, 3513340, 3513596, 3513852, 3514364, + 3514620, 3514876, 3515388, 3515644, 3515900, 3516156, 3516668, 3516924, 3451644, 3451900, 3452412, 3387132, + 3387388, 3322108, 3322620, 3257340, 3192060, 3127036, 3127292, 3062012, 2996732, 2997244, 2931964, 2866684, + 2867195, 2801915, 2802171, 2736891, 2737403, 2737659, 2737915, 2672635, 2672891, 2673403, 2673659, 2673915, + 2674171, 2740219, 2740475, 2740730, 2806522, 2806778, 2872826, 2873082, 2938874, 3004666, 3070458, 3202042, + 3333369, 3530233, 3727097, 3923961, 4120825, 4383225, 4580089, 4842489, 5039353, 5301753, 5498616, 5761016, + 5957880, 6220280, 6417144, 6679544, 6876408, 7138552, 7335416, 7597815, 7794679, 7991543, 8253943, 8450807, + 8647415, 8909815, 9106679, 9303542, 9500406, 9697270, 9959414, 10156278, 10353142, 10550006, 10746614, 10943478, + 11140341, 11402741, 11599605, 11796213 + }; + public static final Colorcet KBC = new Colorcet(kbc, FAMILY.LINEAR); - private static final int[] kg = { - 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 2816, 3072, 3328, 3584, - 3584, 3840, 4096, 4096, 4352, 4608, 4608, 4864, 4864, 5120, 5376, 5376, 5632, 5632, 5888, 5888, - 6144, 6144, 6400, 6400, 6656, 6656, 6656, 6912, 6912, 7168, 7168, 7424, 7424, 7424, 7680, 7680, - 7936, 7936, 7936, 8192, 8192, 8448, 8448, 8448, 8704, 8704, 8960, 8960, 9216, 9216, 9216, 9472, - 9472, 9728, 9728, 9728, 9984, 9984, 10240, 10240, 10496, 10496, 10496, 10752, 10752, 11008, - 11008, 11264, 11264, 11264, 11520, 11520, 11776, 11776, 12032, 12032, 12288, 12288, 12288, - 12544, 12544, 12800, 12800, 13056, 13056, 13056, 13312, 13312, 13568, 13568, 13824, 13824, - 14080, 14080, 14080, 14336, 14336, 14592, 14592, 14848, 14848, 15104, 15104, 15360, 15360, - 15360, 15616, 15616, 15872, 15872, 16128, 16128, 16384, 16384, 16640, 16640, 16640, 16896, - 16896, 17152, 17152, 17408, 17408, 17664, 17664, 17920, 17920, 18176, 18176, 18176, 18432, - 18432, 18688, 18688, 18944, 18944, 19200, 19200, 19456, 19456, 19712, 19712, 19968, 19968, - 19968, 20224, 20224, 20480, 20480, 20736, 20736, 20992, 20992, 21248, 21248, 21504, 21504, - 21760, 21760, 22016, 22016, 22272, 22272, 22528, 22528, 22784, 22784, 23040, 23040, 23040, - 23296, 23296, 23552, 23552, 23808, 23808, 24064, 24064, 24320, 24320, 24576, 24576, 24832, - 24832, 25088, 25088, 25344, 25344, 25600, 25600, 25856, 25856, 26112, 26112, 26368, 26368, - 26624, 26624, 26880, 26880, 27136, 27136, 27392, 27392, 27648, 27648, 27904, 27904, 28160, - 28160, 28416, 28416, 28672, 28672, 28928, 28928, 29184, 29184, 29440, 29440, 29696, 29696, - 29952, 29952, 30208, 30208, 30464, 30464, 30720, 30720, 30976, 30976, 31232, 31232, 31488, - 31488, 31744, 31744, 32000, 32000, 32256, 32256, 32512 - }; - public static final Colorcet KG = new Colorcet(kg, FAMILY.LINEAR); + private static final int[] kg = { + 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 2816, 3072, 3328, 3584, 3584, 3840, 4096, + 4096, 4352, 4608, 4608, 4864, 4864, 5120, 5376, 5376, 5632, 5632, 5888, 5888, 6144, 6144, 6400, 6400, 6656, + 6656, 6656, 6912, 6912, 7168, 7168, 7424, 7424, 7424, 7680, 7680, 7936, 7936, 7936, 8192, 8192, 8448, 8448, + 8448, 8704, 8704, 8960, 8960, 9216, 9216, 9216, 9472, 9472, 9728, 9728, 9728, 9984, 9984, 10240, 10240, 10496, + 10496, 10496, 10752, 10752, 11008, 11008, 11264, 11264, 11264, 11520, 11520, 11776, 11776, 12032, 12032, 12288, + 12288, 12288, 12544, 12544, 12800, 12800, 13056, 13056, 13056, 13312, 13312, 13568, 13568, 13824, 13824, 14080, + 14080, 14080, 14336, 14336, 14592, 14592, 14848, 14848, 15104, 15104, 15360, 15360, 15360, 15616, 15616, 15872, + 15872, 16128, 16128, 16384, 16384, 16640, 16640, 16640, 16896, 16896, 17152, 17152, 17408, 17408, 17664, 17664, + 17920, 17920, 18176, 18176, 18176, 18432, 18432, 18688, 18688, 18944, 18944, 19200, 19200, 19456, 19456, 19712, + 19712, 19968, 19968, 19968, 20224, 20224, 20480, 20480, 20736, 20736, 20992, 20992, 21248, 21248, 21504, 21504, + 21760, 21760, 22016, 22016, 22272, 22272, 22528, 22528, 22784, 22784, 23040, 23040, 23040, 23296, 23296, 23552, + 23552, 23808, 23808, 24064, 24064, 24320, 24320, 24576, 24576, 24832, 24832, 25088, 25088, 25344, 25344, 25600, + 25600, 25856, 25856, 26112, 26112, 26368, 26368, 26624, 26624, 26880, 26880, 27136, 27136, 27392, 27392, 27648, + 27648, 27904, 27904, 28160, 28160, 28416, 28416, 28672, 28672, 28928, 28928, 29184, 29184, 29440, 29440, 29696, + 29696, 29952, 29952, 30208, 30208, 30464, 30464, 30720, 30720, 30976, 30976, 31232, 31232, 31488, 31488, 31744, + 31744, 32000, 32000, 32256, 32256, 32512 + }; + public static final Colorcet KG = new Colorcet(kg, FAMILY.LINEAR); - private static final int[] kgy = { - 5381, 70917, 71173, 136965, 202757, 203013, 268805, 268805, 334597, 400389, 400645, 466181, - 466437, 532229, 532485, 532741, 598277, 598533, 598789, 599045, 599301, 599557, 599813, 599813, - 600069, 600325, 600581, 535301, 535557, 535813, 535812, 536068, 536324, 536580, 536836, 537092, - 537348, 537604, 537604, 537860, 538116, 538372, 604164, 604420, 604676, 604932, 670724, 670724, - 670980, 671236, 737028, 737284, 737540, 737796, 803589, 803845, 803845, 869637, 869893, 870149, - 870405, 936197, 936453, 936709, 936965, 1002757, 1003013, 1003013, 1068805, 1069061, 1069317, - 1069573, 1069829, 1135621, 1135877, 1136133, 1136389, 1202181, 1202438, 1202694, 1202950, - 1268742, 1268742, 1268998, 1269254, 1335046, 1335302, 1335558, 1335814, 1401607, 1401863, - 1402119, 1402375, 1468167, 1468423, 1468679, 1468935, 1534727, 1534983, 1535240, 1535496, - 1601288, 1601544, 1601800, 1602056, 1667848, 1668104, 1668360, 1668617, 1734409, 1734665, - 1734921, 1735177, 1800969, 1801225, 1801481, 1801737, 1867530, 1867786, 1868042, 1868298, - 1934090, 1934346, 1934602, 2000394, 2000650, 2000907, 2001163, 2066955, 2067211, 2067467, - 2067723, 2133515, 2133771, 2134028, 2134284, 2200076, 2200332, 2200588, 2200844, 2266636, - 2266892, 2267148, 2267405, 2333197, 2333453, 2333709, 2333965, 2399757, 2400013, 2400269, - 2466318, 2466574, 2466830, 2467086, 2532878, 2533134, 2533390, 2533646, 2599438, 2599695, - 2599951, 2600207, 2665999, 2666255, 2666511, 2732303, 2732559, 2733071, 2733328, 2799120, - 2799376, 2799632, 2799888, 2865680, 2865936, 2866192, 2866449, 2932241, 2932497, 2932753, - 2998545, 2999057, 2999313, 2999569, 3065361, 3065618, 3065874, 3066130, 3131922, 3132178, - 3132434, 3198226, 3198738, 3198994, 3199251, 3265043, 3265299, 3265555, 3265811, 3331603, - 3331859, 3332115, 3332627, 3398420, 3398676, 3398932, 3464724, 3464980, 3465236, 3465492, - 3531284, 3531797, 3532053, 3663381, 3729173, 3860501, 4057365, 4254229, 4516629, 4713493, - 4975893, 5238037, 5500437, 5762837, 6025237, 6287637, 6549781, 6877717, 7140117, 7402517, - 7664661, 7927061, 8254997, 8517141, 8779541, 9041941, 9304085, 9632021, 9894165, 10156565, - 10418965, 10681108, 10943508, 11271188, 11533588, 11795732, 12058132, 12320276, 12582420, - 12844820, 13106964, 13369108, 13631252, 13958932, 14221076 - }; - public static final Colorcet KGY = new Colorcet(kgy, FAMILY.LINEAR); + private static final int[] kgy = { + 5381, 70917, 71173, 136965, 202757, 203013, 268805, 268805, 334597, 400389, 400645, 466181, 466437, 532229, + 532485, 532741, 598277, 598533, 598789, 599045, 599301, 599557, 599813, 599813, 600069, 600325, 600581, 535301, + 535557, 535813, 535812, 536068, 536324, 536580, 536836, 537092, 537348, 537604, 537604, 537860, 538116, 538372, + 604164, 604420, 604676, 604932, 670724, 670724, 670980, 671236, 737028, 737284, 737540, 737796, 803589, 803845, + 803845, 869637, 869893, 870149, 870405, 936197, 936453, 936709, 936965, 1002757, 1003013, 1003013, 1068805, + 1069061, 1069317, 1069573, 1069829, 1135621, 1135877, 1136133, 1136389, 1202181, 1202438, 1202694, 1202950, + 1268742, 1268742, 1268998, 1269254, 1335046, 1335302, 1335558, 1335814, 1401607, 1401863, 1402119, 1402375, + 1468167, 1468423, 1468679, 1468935, 1534727, 1534983, 1535240, 1535496, 1601288, 1601544, 1601800, 1602056, + 1667848, 1668104, 1668360, 1668617, 1734409, 1734665, 1734921, 1735177, 1800969, 1801225, 1801481, 1801737, + 1867530, 1867786, 1868042, 1868298, 1934090, 1934346, 1934602, 2000394, 2000650, 2000907, 2001163, 2066955, + 2067211, 2067467, 2067723, 2133515, 2133771, 2134028, 2134284, 2200076, 2200332, 2200588, 2200844, 2266636, + 2266892, 2267148, 2267405, 2333197, 2333453, 2333709, 2333965, 2399757, 2400013, 2400269, 2466318, 2466574, + 2466830, 2467086, 2532878, 2533134, 2533390, 2533646, 2599438, 2599695, 2599951, 2600207, 2665999, 2666255, + 2666511, 2732303, 2732559, 2733071, 2733328, 2799120, 2799376, 2799632, 2799888, 2865680, 2865936, 2866192, + 2866449, 2932241, 2932497, 2932753, 2998545, 2999057, 2999313, 2999569, 3065361, 3065618, 3065874, 3066130, + 3131922, 3132178, 3132434, 3198226, 3198738, 3198994, 3199251, 3265043, 3265299, 3265555, 3265811, 3331603, + 3331859, 3332115, 3332627, 3398420, 3398676, 3398932, 3464724, 3464980, 3465236, 3465492, 3531284, 3531797, + 3532053, 3663381, 3729173, 3860501, 4057365, 4254229, 4516629, 4713493, 4975893, 5238037, 5500437, 5762837, + 6025237, 6287637, 6549781, 6877717, 7140117, 7402517, 7664661, 7927061, 8254997, 8517141, 8779541, 9041941, + 9304085, 9632021, 9894165, 10156565, 10418965, 10681108, 10943508, 11271188, 11533588, 11795732, 12058132, + 12320276, 12582420, 12844820, 13106964, 13369108, 13631252, 13958932, 14221076 + }; + public static final Colorcet KGY = new Colorcet(kgy, FAMILY.LINEAR); - private static final int[] kr = { - 0, 131072, 262144, 393472, 524544, 655616, 786944, 918016, 983808, 1114880, 1245952, 1311488, - 1442816, 1508352, 1573888, 1704960, 1770752, 1836288, 1901824, 1967360, 2033152, 2098688, - 2164224, 2229760, 2295296, 2360832, 2426624, 2492160, 2557696, 2623232, 2688768, 2754560, - 2820096, 2885632, 2885632, 2951168, 3016704, 3082240, 3148032, 3148032, 3213568, 3279104, - 3344640, 3344640, 3410176, 3475968, 3475968, 3541504, 3607040, 3672576, 3672576, 3738112, - 3803904, 3869440, 3869440, 3934976, 4000512, 4066048, 4066048, 4131840, 4197376, 4262912, - 4328448, 4328448, 4393984, 4459520, 4525312, 4525312, 4590848, 4656384, 4721920, 4721920, - 4787456, 4853248, 4918784, 4918784, 4984320, 5049856, 5115392, 5180928, 5181184, 5246720, - 5312256, 5377792, 5377792, 5443328, 5508864, 5574656, 5640192, 5640192, 5705728, 5771264, - 5836800, 5902592, 5902592, 5968128, 6033664, 6099200, 6164736, 6164736, 6230528, 6296064, - 6361600, 6427136, 6427136, 6492672, 6558208, 6624000, 6689536, 6689536, 6755072, 6820608, - 6886144, 6951936, 6951936, 7017472, 7083008, 7148544, 7214080, 7214080, 7279872, 7345408, - 7410944, 7476480, 7542016, 7542016, 7607808, 7673344, 7738880, 7804416, 7804416, 7869952, - 7935488, 8001280, 8066816, 8132352, 8132352, 8197888, 8263424, 8329216, 8394752, 8460288, - 8460288, 8525824, 8591360, 8657152, 8722688, 8788224, 8788224, 8853760, 8919296, 8984832, - 9050624, 9116160, 9181696, 9181696, 9247232, 9312768, 9378560, 9444096, 9509632, 9509632, - 9575168, 9640704, 9706496, 9772032, 9837568, 9903104, 9903104, 9968640, 10034432, 10099968, - 10165504, 10231040, 10296576, 10296576, 10362368, 10427904, 10493440, 10558976, 10624512, - 10690048, 10755584, 10755840, 10821376, 10886912, 10952448, 11017984, 11083520, 11149312, - 11214848, 11214848, 11280384, 11345920, 11411456, 11477248, 11542784, 11608320, 11673856, - 11673856, 11739392, 11805184, 11870720, 11936256, 12001792, 12067328, 12132864, 12198656, - 12198656, 12264192, 12329728, 12395264, 12460800, 12526592, 12592128, 12657664, 12723200, - 12723200, 12788736, 12854528, 12920064, 12985600, 13051136, 13116672, 13182464, 13248000, - 13313536, 13313536, 13379072, 13444608, 13510400, 13575936, 13641472, 13707008, 13772544, - 13838080, 13903872, 13969408, 13969408, 14034944, 14100480, 14166016, 14231808, 14297344, - 14362880, 14428416, 14493952, 14559488, 14625280, 14625280, 14690816, 14756352, 14821888, - 14887680, 14953216, 15018752 - }; - public static final Colorcet KR = new Colorcet(kr, FAMILY.LINEAR); + private static final int[] kr = { + 0, 131072, 262144, 393472, 524544, 655616, 786944, 918016, 983808, 1114880, 1245952, 1311488, 1442816, 1508352, + 1573888, 1704960, 1770752, 1836288, 1901824, 1967360, 2033152, 2098688, 2164224, 2229760, 2295296, 2360832, + 2426624, 2492160, 2557696, 2623232, 2688768, 2754560, 2820096, 2885632, 2885632, 2951168, 3016704, 3082240, + 3148032, 3148032, 3213568, 3279104, 3344640, 3344640, 3410176, 3475968, 3475968, 3541504, 3607040, 3672576, + 3672576, 3738112, 3803904, 3869440, 3869440, 3934976, 4000512, 4066048, 4066048, 4131840, 4197376, 4262912, + 4328448, 4328448, 4393984, 4459520, 4525312, 4525312, 4590848, 4656384, 4721920, 4721920, 4787456, 4853248, + 4918784, 4918784, 4984320, 5049856, 5115392, 5180928, 5181184, 5246720, 5312256, 5377792, 5377792, 5443328, + 5508864, 5574656, 5640192, 5640192, 5705728, 5771264, 5836800, 5902592, 5902592, 5968128, 6033664, 6099200, + 6164736, 6164736, 6230528, 6296064, 6361600, 6427136, 6427136, 6492672, 6558208, 6624000, 6689536, 6689536, + 6755072, 6820608, 6886144, 6951936, 6951936, 7017472, 7083008, 7148544, 7214080, 7214080, 7279872, 7345408, + 7410944, 7476480, 7542016, 7542016, 7607808, 7673344, 7738880, 7804416, 7804416, 7869952, 7935488, 8001280, + 8066816, 8132352, 8132352, 8197888, 8263424, 8329216, 8394752, 8460288, 8460288, 8525824, 8591360, 8657152, + 8722688, 8788224, 8788224, 8853760, 8919296, 8984832, 9050624, 9116160, 9181696, 9181696, 9247232, 9312768, + 9378560, 9444096, 9509632, 9509632, 9575168, 9640704, 9706496, 9772032, 9837568, 9903104, 9903104, 9968640, + 10034432, 10099968, 10165504, 10231040, 10296576, 10296576, 10362368, 10427904, 10493440, 10558976, 10624512, + 10690048, 10755584, 10755840, 10821376, 10886912, 10952448, 11017984, 11083520, 11149312, 11214848, 11214848, + 11280384, 11345920, 11411456, 11477248, 11542784, 11608320, 11673856, 11673856, 11739392, 11805184, 11870720, + 11936256, 12001792, 12067328, 12132864, 12198656, 12198656, 12264192, 12329728, 12395264, 12460800, 12526592, + 12592128, 12657664, 12723200, 12723200, 12788736, 12854528, 12920064, 12985600, 13051136, 13116672, 13182464, + 13248000, 13313536, 13313536, 13379072, 13444608, 13510400, 13575936, 13641472, 13707008, 13772544, 13838080, + 13903872, 13969408, 13969408, 14034944, 14100480, 14166016, 14231808, 14297344, 14362880, 14428416, 14493952, + 14559488, 14625280, 14625280, 14690816, 14756352, 14821888, 14887680, 14953216, 15018752 + }; + public static final Colorcet KR = new Colorcet(kr, FAMILY.LINEAR); - private static final int[] rainbow = { - 13560, 14326, 15091, 15856, 16365, 16874, 17639, 18148, 18657, 19166, 19675, 20440, 20949, - 21458, 21712, 22221, 22730, 23239, 23748, 24257, 24766, 25019, 25528, 26038, 26291, 26800, - 27309, 27562, 28071, 28325, 28578, 29087, 29341, 29594, 30104, 30357, 489363, 882832, 1276302, - 1604235, 1866633, 2063495, 2325892, 2522754, 2654079, 2850941, 2982267, 3113592, 3244918, - 3310707, 3442033, 3507823, 3573612, 3639402, 3705191, 3770981, 3836770, 3902560, 3968605, - 3968859, 4034648, 4034901, 4100691, 4100944, 4101197, 4101450, 4101703, 4167493, 4167746, - 4102462, 4102715, 4102968, 4103221, 4103474, 4103726, 4103979, 4169767, 4170020, 4235809, - 4301597, 4367386, 4498711, 4564501, 4695827, 4892433, 5023759, 5220622, 5351693, 5548557, - 5745421, 5942029, 6138893, 6270221, 6466830, 6663694, 6794766, 6991631, 7188495, 7319567, - 7516432, 7647504, 7844369, 7975441, 8172305, 8303634, 8434706, 8631570, 8762643, 8959507, - 9090579, 9221908, 9418772, 9549845, 9681173, 9812245, 10009110, 10140182, 10271510, 10468119, - 10599447, 10730520, 10861848, 10993176, 11189785, 11321113, 11452185, 11583514, 11714586, - 11911451, 12042523, 12173851, 12304924, 12436252, 12632860, 12764189, 12895261, 13026589, - 13157662, 13288990, 13485599, 13616927, 13747999, 13879328, 14010400, 14141728, 14272801, - 14469665, 14600738, 14731810, 14863138, 14994211, 15125539, 15256611, 15387684, 15519012, - 15650084, 15781156, 15912228, 15977764, 16108580, 16174116, 16304932, 16370212, 16369955, - 16435235, 16500514, 16500258, 16565537, 16565281, 16564768, 16630048, 16629791, 16629279, - 16629022, 16694045, 16693789, 16693532, 16693019, 16692763, 16692506, 16757530, 16757273, - 16756760, 16756504, 16756247, 16755734, 16755478, 16754965, 16754709, 16754452, 16753939, - 16753683, 16753170, 16752913, 16752400, 16752144, 16751631, 16751374, 16751118, 16750605, - 16750348, 16749835, 16749579, 16749066, 16748809, 16748296, 16748040, 16747527, 16747270, - 16746757, 16746501, 16745988, 16745476, 16745219, 16744706, 16744450, 16743937, 16743425, - 16743168, 16742656, 16742400, 16741888, 16741376, 16740864, 16740608, 16740096, 16739584, - 16739328, 16738816, 16738304, 16737792, 16737280, 16736768, 16736512, 16736000, 16735488, - 16734976, 16734464, 16733952, 16733440, 16732928, 16732160, 16731648, 16731136, 16730624, - 16729856, 16729344, 16728576, 16728064, 16727296, 16726528, 16725760, 16724992, 16724224, - 16723200, 16722432 - }; - public static final Colorcet RAINBOW = new Colorcet(rainbow, FAMILY.LINEAR); + private static final int[] rainbow = { + 13560, 14326, 15091, 15856, 16365, 16874, 17639, 18148, 18657, 19166, 19675, 20440, 20949, 21458, 21712, 22221, + 22730, 23239, 23748, 24257, 24766, 25019, 25528, 26038, 26291, 26800, 27309, 27562, 28071, 28325, 28578, 29087, + 29341, 29594, 30104, 30357, 489363, 882832, 1276302, 1604235, 1866633, 2063495, 2325892, 2522754, 2654079, + 2850941, 2982267, 3113592, 3244918, 3310707, 3442033, 3507823, 3573612, 3639402, 3705191, 3770981, 3836770, + 3902560, 3968605, 3968859, 4034648, 4034901, 4100691, 4100944, 4101197, 4101450, 4101703, 4167493, 4167746, + 4102462, 4102715, 4102968, 4103221, 4103474, 4103726, 4103979, 4169767, 4170020, 4235809, 4301597, 4367386, + 4498711, 4564501, 4695827, 4892433, 5023759, 5220622, 5351693, 5548557, 5745421, 5942029, 6138893, 6270221, + 6466830, 6663694, 6794766, 6991631, 7188495, 7319567, 7516432, 7647504, 7844369, 7975441, 8172305, 8303634, + 8434706, 8631570, 8762643, 8959507, 9090579, 9221908, 9418772, 9549845, 9681173, 9812245, 10009110, 10140182, + 10271510, 10468119, 10599447, 10730520, 10861848, 10993176, 11189785, 11321113, 11452185, 11583514, 11714586, + 11911451, 12042523, 12173851, 12304924, 12436252, 12632860, 12764189, 12895261, 13026589, 13157662, 13288990, + 13485599, 13616927, 13747999, 13879328, 14010400, 14141728, 14272801, 14469665, 14600738, 14731810, 14863138, + 14994211, 15125539, 15256611, 15387684, 15519012, 15650084, 15781156, 15912228, 15977764, 16108580, 16174116, + 16304932, 16370212, 16369955, 16435235, 16500514, 16500258, 16565537, 16565281, 16564768, 16630048, 16629791, + 16629279, 16629022, 16694045, 16693789, 16693532, 16693019, 16692763, 16692506, 16757530, 16757273, 16756760, + 16756504, 16756247, 16755734, 16755478, 16754965, 16754709, 16754452, 16753939, 16753683, 16753170, 16752913, + 16752400, 16752144, 16751631, 16751374, 16751118, 16750605, 16750348, 16749835, 16749579, 16749066, 16748809, + 16748296, 16748040, 16747527, 16747270, 16746757, 16746501, 16745988, 16745476, 16745219, 16744706, 16744450, + 16743937, 16743425, 16743168, 16742656, 16742400, 16741888, 16741376, 16740864, 16740608, 16740096, 16739584, + 16739328, 16738816, 16738304, 16737792, 16737280, 16736768, 16736512, 16736000, 16735488, 16734976, 16734464, + 16733952, 16733440, 16732928, 16732160, 16731648, 16731136, 16730624, 16729856, 16729344, 16728576, 16728064, + 16727296, 16726528, 16725760, 16724992, 16724224, 16723200, 16722432 + }; + public static final Colorcet RAINBOW = new Colorcet(rainbow, FAMILY.LINEAR); - private final int[] colors; + private final int[] colors; - private Colorcet(int[] colors, ColorTable.FAMILY family) { - super(family); - this.colors = colors; - } + private Colorcet(int[] colors, ColorTable.FAMILY family) { + super(family); + this.colors = colors; + } - @Override - public List getColors() { - List colors = new ArrayList<>(); - for (int color : this.colors) colors.add(new Color(color)); - return colors; - } + @Override + public List getColors() { + List colors = new ArrayList<>(); + for (int color : this.colors) colors.add(new Color(color)); + return colors; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/IDL.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/IDL.java index a5998e7..c25c9e9 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/IDL.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/IDL.java @@ -50,49 +50,44 @@ import java.util.List; */ public class IDL extends ColorTable { - private static final int[] colors74 = { - 10355010, 10486595, 10618435, 10815556, 10947396, 11078981, 11210821, 11342406, 11539782, - 11671367, 11802951, 11934792, 12066376, 12263753, 12395337, 12527178, 12658762, 12790347, - 12987723, 13119308, 13251148, 13382733, 13514573, 13711694, 13843534, 13975119, 14041167, - 14107214, 14238542, 14304589, 14370637, 14436684, 14502732, 14634059, 14700107, 14766154, - 14832202, 14898249, 15029833, 15095625, 15161672, 15227720, 15293767, 15359815, 15491142, - 15557190, 15623237, 15689285, 15755332, 15886660, 15952707, 16018755, 16019524, 16085573, - 16086343, 16086856, 16153161, 16153930, 16219979, 16220749, 16221262, 16287567, 16288336, - 16288849, 16355155, 16355668, 16356437, 16422742, 16423255, 16424025, 16490074, 16490843, - 16557148, 16557661, 16558431, 16624480, 16625249, 16625763, 16626276, 16626790, 16627303, - 16627817, 16628331, 16628588, 16629102, 16629616, 16630129, 16630643, 16631156, 16697206, - 16697720, 16698233, 16698747, 16699260, 16699774, 16700288, 16700545, 16701059, 16701573, - 16702086, 16702600, 16703113, 16703627, 16703885, 16704143, 16704657, 16704915, 16705173, - 16705431, 16705946, 16706204, 16706462, 16706720, 16707234, 16707492, 16773286, 16773544, - 16774058, 16774316, 16774574, 16774832, 16775347, 16775605, 16775863, 16776121, 16776635, - 16776893, 16777151, 16711614, 16645820, 16580283, 16514489, 16448952, 16383414, 16317621, - 16252083, 16186546, 16120752, 16055215, 15989421, 15989420, 15923882, 15858089, 15792551, - 15726758, 15661220, 15595683, 15529889, 15464352, 15398814, 15333021, 15267483, 15201690, - 15136152, 15004824, 14807961, 14676633, 14545306, 14348442, 14217115, 14020251, 13888924, - 13757596, 13560733, 13429405, 13298078, 13101470, 12970143, 12838815, 12641952, 12510624, - 12379297, 12182433, 12051106, 11854242, 11722915, 11591587, 11394724, 11263396, 11066532, - 10935204, 10738340, 10541476, 10410148, 10213284, 10016420, 9885092, 9688228, 9491364, 9360036, - 9163172, 9031845, 8834725, 8637861, 8506533, 8309669, 8112805, 7981477, 7784613, 7587749, - 7456421, 7259557, 7062693, 6931365, 6734501, 6602918, 6471079, 6339496, 6207913, 6076074, - 5944491, 5747372, 5615533, 5483950, 5352367, 5220528, 5088945, 4957361, 4825778, 4693939, - 4562356, 4430773, 4298934, 4101815, 3970232, 3838393, 3706810, 3575227, 3443388, 3311805, - 3442364, 3507387, 3637690, 3768249, 3833272, 3963831, 4094390, 4224693, 4289716, 4420275, - 4550834, 4615857, 4746416, 4876718, 4941741, 5072300, 5202859, 5267882, 5398185, 5528744, - 5659303, 5724326, 5854885, 5985188, 6050211, 6180770 - }; - public static final IDL CBSPECTRAL = new IDL(colors74, FAMILY.LINEAR); + private static final int[] colors74 = { + 10355010, 10486595, 10618435, 10815556, 10947396, 11078981, 11210821, 11342406, 11539782, 11671367, 11802951, + 11934792, 12066376, 12263753, 12395337, 12527178, 12658762, 12790347, 12987723, 13119308, 13251148, 13382733, + 13514573, 13711694, 13843534, 13975119, 14041167, 14107214, 14238542, 14304589, 14370637, 14436684, 14502732, + 14634059, 14700107, 14766154, 14832202, 14898249, 15029833, 15095625, 15161672, 15227720, 15293767, 15359815, + 15491142, 15557190, 15623237, 15689285, 15755332, 15886660, 15952707, 16018755, 16019524, 16085573, 16086343, + 16086856, 16153161, 16153930, 16219979, 16220749, 16221262, 16287567, 16288336, 16288849, 16355155, 16355668, + 16356437, 16422742, 16423255, 16424025, 16490074, 16490843, 16557148, 16557661, 16558431, 16624480, 16625249, + 16625763, 16626276, 16626790, 16627303, 16627817, 16628331, 16628588, 16629102, 16629616, 16630129, 16630643, + 16631156, 16697206, 16697720, 16698233, 16698747, 16699260, 16699774, 16700288, 16700545, 16701059, 16701573, + 16702086, 16702600, 16703113, 16703627, 16703885, 16704143, 16704657, 16704915, 16705173, 16705431, 16705946, + 16706204, 16706462, 16706720, 16707234, 16707492, 16773286, 16773544, 16774058, 16774316, 16774574, 16774832, + 16775347, 16775605, 16775863, 16776121, 16776635, 16776893, 16777151, 16711614, 16645820, 16580283, 16514489, + 16448952, 16383414, 16317621, 16252083, 16186546, 16120752, 16055215, 15989421, 15989420, 15923882, 15858089, + 15792551, 15726758, 15661220, 15595683, 15529889, 15464352, 15398814, 15333021, 15267483, 15201690, 15136152, + 15004824, 14807961, 14676633, 14545306, 14348442, 14217115, 14020251, 13888924, 13757596, 13560733, 13429405, + 13298078, 13101470, 12970143, 12838815, 12641952, 12510624, 12379297, 12182433, 12051106, 11854242, 11722915, + 11591587, 11394724, 11263396, 11066532, 10935204, 10738340, 10541476, 10410148, 10213284, 10016420, 9885092, + 9688228, 9491364, 9360036, 9163172, 9031845, 8834725, 8637861, 8506533, 8309669, 8112805, 7981477, 7784613, + 7587749, 7456421, 7259557, 7062693, 6931365, 6734501, 6602918, 6471079, 6339496, 6207913, 6076074, 5944491, + 5747372, 5615533, 5483950, 5352367, 5220528, 5088945, 4957361, 4825778, 4693939, 4562356, 4430773, 4298934, + 4101815, 3970232, 3838393, 3706810, 3575227, 3443388, 3311805, 3442364, 3507387, 3637690, 3768249, 3833272, + 3963831, 4094390, 4224693, 4289716, 4420275, 4550834, 4615857, 4746416, 4876718, 4941741, 5072300, 5202859, + 5267882, 5398185, 5528744, 5659303, 5724326, 5854885, 5985188, 6050211, 6180770 + }; + public static final IDL CBSPECTRAL = new IDL(colors74, FAMILY.LINEAR); - private final int[] colors; + private final int[] colors; - private IDL(int[] colors, ColorTable.FAMILY family) { - super(family); - this.colors = colors; - } + private IDL(int[] colors, ColorTable.FAMILY family) { + super(family); + this.colors = colors; + } - @Override - public List getColors() { - List colors = new ArrayList<>(); - for (int color : this.colors) colors.add(new Color(color)); - return colors; - } + @Override + public List getColors() { + List colors = new ArrayList<>(); + for (int color : this.colors) colors.add(new Color(color)); + return colors; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/MATLAB.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/MATLAB.java index d9b58d3..6198f91 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/MATLAB.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/MATLAB.java @@ -50,49 +50,44 @@ import java.util.List; */ public class MATLAB extends ColorTable { - private static final int[] parula = { - 4073385, 4138924, 4139183, 4204978, 4205237, 4271032, 4271291, 4271549, 4337344, 4337603, - 4403398, 4403656, 4469451, 4469710, 4535761, 4536019, 4536278, 4602072, 4602330, 4602588, - 4603103, 4668896, 4669154, 4669668, 4669926, 4670439, 4736233, 4736490, 4737004, 4737261, - 4737774, 4738031, 4738289, 4738802, 4739059, 4739572, 4739829, 4740086, 4740599, 4740856, - 4741368, 4676089, 4676346, 4676859, 4677115, 4677628, 4612348, 4612605, 4547581, 4547838, - 4482814, 4483070, 4418047, 4352767, 4287743, 4222463, 4157439, 4092159, 4027135, 3896319, - 3831295, 3700479, 3569919, 3504638, 3374078, 3308797, 3243773, 3178492, 3113467, 3113723, - 3114234, 3048953, 3049209, 3049720, 3049975, 2984694, 2985205, 2985460, 2985715, 2985969, - 2920944, 2855664, 2790383, 2725358, 2660077, 2594796, 2595051, 2530026, 2530281, 2465000, - 2465256, 2465511, 2400230, 2335206, 2335461, 2270181, 2204900, 2139620, 2074339, 2009058, - 1944033, 1944289, 1879008, 1813727, 1748446, 1683164, 1552347, 1487066, 1356249, 1225431, - 1094358, 963540, 767187, 570833, 440016, 309198, 178125, 112843, 47562, 47816, 47814, 113605, - 179395, 310465, 441792, 638398, 900796, 1097659, 1359801, 1556663, 1753269, 1950132, 2146738, - 2343600, 2474670, 2605997, 2737067, 2868393, 2999719, 3065253, 3131043, 3262113, 3327903, - 3393437, 3524763, 3590297, 3721623, 3787157, 3918483, 4049552, 4246414, 4377484, 4574345, - 4770951, 4967556, 5164162, 5361023, 5623165, 5819770, 6016376, 6212981, 6409586, 6606447, - 6868588, 7065194, 7327335, 7523684, 7785825, 8047966, 8244572, 8506713, 8768598, 8965204, - 9227345, 9489230, 9685835, 9947977, 10144326, 10406467, 10602817, 10864958, 11061308, 11323449, - 11519799, 11716149, 11978291, 12174641, 12371247, 12567598, 12763948, 12960554, 13156905, - 13353256, 13549864, 13746215, 13942823, 14139176, 14269992, 14466601, 14662953, 14794026, - 14990379, 15121453, 15317806, 15448880, 15579955, 15776565, 15907383, 16038457, 16169531, - 16300861, 16431934, 16563263, 16629054, 16760381, 16760636, 16760891, 16761402, 16761657, - 16762168, 16762423, 16762934, 16763189, 16763700, 16698419, 16698930, 16699185, 16634160, - 16568879, 16569391, 16504366, 16439085, 16439596, 16374315, 16309291, 16309802, 16244521, - 16245032, 16179752, 16180263, 16180519, 16181030, 16181541, 16181796, 16182307, 16182562, - 16183073, 16248864, 16249375, 16249630, 16315677, 16315931, 16381722, 16382232, 16448022, - 16448533 - }; + private static final int[] parula = { + 4073385, 4138924, 4139183, 4204978, 4205237, 4271032, 4271291, 4271549, 4337344, 4337603, 4403398, 4403656, + 4469451, 4469710, 4535761, 4536019, 4536278, 4602072, 4602330, 4602588, 4603103, 4668896, 4669154, 4669668, + 4669926, 4670439, 4736233, 4736490, 4737004, 4737261, 4737774, 4738031, 4738289, 4738802, 4739059, 4739572, + 4739829, 4740086, 4740599, 4740856, 4741368, 4676089, 4676346, 4676859, 4677115, 4677628, 4612348, 4612605, + 4547581, 4547838, 4482814, 4483070, 4418047, 4352767, 4287743, 4222463, 4157439, 4092159, 4027135, 3896319, + 3831295, 3700479, 3569919, 3504638, 3374078, 3308797, 3243773, 3178492, 3113467, 3113723, 3114234, 3048953, + 3049209, 3049720, 3049975, 2984694, 2985205, 2985460, 2985715, 2985969, 2920944, 2855664, 2790383, 2725358, + 2660077, 2594796, 2595051, 2530026, 2530281, 2465000, 2465256, 2465511, 2400230, 2335206, 2335461, 2270181, + 2204900, 2139620, 2074339, 2009058, 1944033, 1944289, 1879008, 1813727, 1748446, 1683164, 1552347, 1487066, + 1356249, 1225431, 1094358, 963540, 767187, 570833, 440016, 309198, 178125, 112843, 47562, 47816, 47814, 113605, + 179395, 310465, 441792, 638398, 900796, 1097659, 1359801, 1556663, 1753269, 1950132, 2146738, 2343600, 2474670, + 2605997, 2737067, 2868393, 2999719, 3065253, 3131043, 3262113, 3327903, 3393437, 3524763, 3590297, 3721623, + 3787157, 3918483, 4049552, 4246414, 4377484, 4574345, 4770951, 4967556, 5164162, 5361023, 5623165, 5819770, + 6016376, 6212981, 6409586, 6606447, 6868588, 7065194, 7327335, 7523684, 7785825, 8047966, 8244572, 8506713, + 8768598, 8965204, 9227345, 9489230, 9685835, 9947977, 10144326, 10406467, 10602817, 10864958, 11061308, + 11323449, 11519799, 11716149, 11978291, 12174641, 12371247, 12567598, 12763948, 12960554, 13156905, 13353256, + 13549864, 13746215, 13942823, 14139176, 14269992, 14466601, 14662953, 14794026, 14990379, 15121453, 15317806, + 15448880, 15579955, 15776565, 15907383, 16038457, 16169531, 16300861, 16431934, 16563263, 16629054, 16760381, + 16760636, 16760891, 16761402, 16761657, 16762168, 16762423, 16762934, 16763189, 16763700, 16698419, 16698930, + 16699185, 16634160, 16568879, 16569391, 16504366, 16439085, 16439596, 16374315, 16309291, 16309802, 16244521, + 16245032, 16179752, 16180263, 16180519, 16181030, 16181541, 16181796, 16182307, 16182562, 16183073, 16248864, + 16249375, 16249630, 16315677, 16315931, 16381722, 16382232, 16448022, 16448533 + }; - public static final MATLAB PARULA = new MATLAB(parula, FAMILY.LINEAR); + public static final MATLAB PARULA = new MATLAB(parula, FAMILY.LINEAR); - private final int[] colors; + private final int[] colors; - private MATLAB(int[] colors, ColorTable.FAMILY family) { - super(family); - this.colors = colors; - } + private MATLAB(int[] colors, ColorTable.FAMILY family) { + super(family); + this.colors = colors; + } - @Override - public List getColors() { - List colors = new ArrayList<>(); - for (int color : this.colors) colors.add(new Color(color)); - return colors; - } + @Override + public List getColors() { + List colors = new ArrayList<>(); + for (int color : this.colors) colors.add(new Color(color)); + return colors; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Matplotlib.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Matplotlib.java index b89bf8e..6ae96c8 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Matplotlib.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/Matplotlib.java @@ -53,47 +53,43 @@ import java.util.List; */ public class Matplotlib extends ColorTable { - private static final int[] viridis = { - 4456788, 4457046, 4523095, 4523353, 4589402, 4589660, 4590173, 4590430, 4656480, 4656737, - 4657251, 4657508, 4658021, 4723815, 4724328, 4724585, 4724842, 4725356, 4725613, 4725870, - 4726127, 4726640, 4726897, 4727155, 4727668, 4727925, 4728182, 4728439, 4728952, 4729209, - 4663930, 4664442, 4664699, 4664956, 4665213, 4599934, 4600446, 4600703, 4600960, 4535681, - 4536193, 4536450, 4471171, 4471427, 4471684, 4406660, 4406917, 4341637, 4341894, 4342150, - 4276871, 4277383, 4212104, 4212360, 4147080, 4147337, 4082057, 4082313, 4082826, 4017546, - 4017802, 3952522, 3952779, 3887499, 3887755, 3822475, 3822732, 3757452, 3757708, 3692684, - 3692940, 3627660, 3627917, 3562637, 3562893, 3497613, 3497869, 3432589, 3432845, 3367565, - 3367821, 3302542, 3302798, 3237518, 3237774, 3238030, 3172750, 3173006, 3107726, 3107982, - 3042702, 3042958, 3043214, 2977934, 2978190, 2912654, 2912910, 2913166, 2847886, 2848142, - 2782862, 2783118, 2783374, 2718094, 2718350, 2718606, 2653326, 2653582, 2588302, 2588558, - 2588814, 2523534, 2523790, 2523790, 2458510, 2458766, 2459022, 2393742, 2393998, 2328718, - 2328974, 2329229, 2263949, 2264205, 2264461, 2199181, 2199437, 2199693, 2199948, 2134668, - 2134668, 2134924, 2069644, 2069899, 2070155, 2070411, 2070667, 2070922, 2071178, 2005898, - 2006153, 2006409, 2072201, 2072456, 2072712, 2072968, 2072967, 2073223, 2139014, 2139270, - 2205061, 2205317, 2271109, 2271364, 2337155, 2402947, 2468738, 2468994, 2534785, 2600321, - 2666112, 2731903, 2797695, 2929022, 2994813, 3060604, 3126396, 3257723, 3323514, 3454585, - 3520377, 3651704, 3717495, 3848822, 3914613, 4045940, 4177011, 4242802, 4374129, 4505456, - 4636783, 4768110, 4899181, 5030508, 5161835, 5293162, 5424489, 5555560, 5686887, 5818213, - 5949540, 6080611, 6211938, 6343264, 6540127, 6671198, 6802524, 6933851, 7130458, 7261784, - 7393111, 7589974, 7721044, 7852371, 8048977, 8180304, 8377166, 8508237, 8705099, 8836425, - 9033032, 9164358, 9360965, 9492291, 9688897, 9820224, 10016830, 10213692, 10344763, 10541625, - 10672695, 10869558, 11066164, 11197490, 11394096, 11590959, 11722029, 11918891, 12115497, - 12246568, 12443430, 12640037, 12771107, 12967969, 13164576, 13295903, 13492509, 13689116, - 13820443, 14017050, 14213657, 14344985, 14541592, 14672664, 14869528, 15066137, 15197209, - 15394074, 15525147, 15721756, 15852829, 16049694, 16180768, 16311841, 16508707, 16639781 - }; - public static final Matplotlib VIRIDIS = new Matplotlib(viridis, FAMILY.LINEAR); + private static final int[] viridis = { + 4456788, 4457046, 4523095, 4523353, 4589402, 4589660, 4590173, 4590430, 4656480, 4656737, 4657251, 4657508, + 4658021, 4723815, 4724328, 4724585, 4724842, 4725356, 4725613, 4725870, 4726127, 4726640, 4726897, 4727155, + 4727668, 4727925, 4728182, 4728439, 4728952, 4729209, 4663930, 4664442, 4664699, 4664956, 4665213, 4599934, + 4600446, 4600703, 4600960, 4535681, 4536193, 4536450, 4471171, 4471427, 4471684, 4406660, 4406917, 4341637, + 4341894, 4342150, 4276871, 4277383, 4212104, 4212360, 4147080, 4147337, 4082057, 4082313, 4082826, 4017546, + 4017802, 3952522, 3952779, 3887499, 3887755, 3822475, 3822732, 3757452, 3757708, 3692684, 3692940, 3627660, + 3627917, 3562637, 3562893, 3497613, 3497869, 3432589, 3432845, 3367565, 3367821, 3302542, 3302798, 3237518, + 3237774, 3238030, 3172750, 3173006, 3107726, 3107982, 3042702, 3042958, 3043214, 2977934, 2978190, 2912654, + 2912910, 2913166, 2847886, 2848142, 2782862, 2783118, 2783374, 2718094, 2718350, 2718606, 2653326, 2653582, + 2588302, 2588558, 2588814, 2523534, 2523790, 2523790, 2458510, 2458766, 2459022, 2393742, 2393998, 2328718, + 2328974, 2329229, 2263949, 2264205, 2264461, 2199181, 2199437, 2199693, 2199948, 2134668, 2134668, 2134924, + 2069644, 2069899, 2070155, 2070411, 2070667, 2070922, 2071178, 2005898, 2006153, 2006409, 2072201, 2072456, + 2072712, 2072968, 2072967, 2073223, 2139014, 2139270, 2205061, 2205317, 2271109, 2271364, 2337155, 2402947, + 2468738, 2468994, 2534785, 2600321, 2666112, 2731903, 2797695, 2929022, 2994813, 3060604, 3126396, 3257723, + 3323514, 3454585, 3520377, 3651704, 3717495, 3848822, 3914613, 4045940, 4177011, 4242802, 4374129, 4505456, + 4636783, 4768110, 4899181, 5030508, 5161835, 5293162, 5424489, 5555560, 5686887, 5818213, 5949540, 6080611, + 6211938, 6343264, 6540127, 6671198, 6802524, 6933851, 7130458, 7261784, 7393111, 7589974, 7721044, 7852371, + 8048977, 8180304, 8377166, 8508237, 8705099, 8836425, 9033032, 9164358, 9360965, 9492291, 9688897, 9820224, + 10016830, 10213692, 10344763, 10541625, 10672695, 10869558, 11066164, 11197490, 11394096, 11590959, 11722029, + 11918891, 12115497, 12246568, 12443430, 12640037, 12771107, 12967969, 13164576, 13295903, 13492509, 13689116, + 13820443, 14017050, 14213657, 14344985, 14541592, 14672664, 14869528, 15066137, 15197209, 15394074, 15525147, + 15721756, 15852829, 16049694, 16180768, 16311841, 16508707, 16639781 + }; + public static final Matplotlib VIRIDIS = new Matplotlib(viridis, FAMILY.LINEAR); - private final int[] colors; + private final int[] colors; - private Matplotlib(int[] colors, ColorTable.FAMILY family) { - super(family); - this.colors = colors; - } + private Matplotlib(int[] colors, ColorTable.FAMILY family) { + super(family); + this.colors = colors; + } - @Override - public List getColors() { - List colors = new ArrayList<>(); - for (int color : this.colors) colors.add(new Color(color)); - return colors; - } + @Override + public List getColors() { + List colors = new ArrayList<>(); + for (int color : this.colors) colors.add(new Color(color)); + return colors; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ScientificColourMaps6.java b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ScientificColourMaps6.java index f594acb..1acb879 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ScientificColourMaps6.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/colorMaps/tables/ScientificColourMaps6.java @@ -55,269 +55,229 @@ import java.util.List; */ public class ScientificColourMaps6 extends ColorTable { - private static final int[] batlow = { - 72025, 138073, 203866, 269914, 335706, 401755, 467547, 468059, 533851, 599900, 665692, 666204, - 731996, 732509, 798301, 798557, 864605, 864862, 865118, 931166, 931422, 931678, 997471, 997727, - 998239, 998495, 1064287, 1064543, 1064800, 1065056, 1130848, 1131104, 1131360, 1131616, 1197409, - 1197665, 1197921, 1198177, 1263969, 1264225, 1264481, 1330274, 1330530, 1330786, 1396578, - 1396578, 1462370, 1462626, 1528418, 1528674, 1594466, 1594722, 1660514, 1660770, 1726306, - 1792098, 1792354, 1858146, 1923938, 1989730, 1989986, 2055521, 2121313, 2187105, 2252897, - 2318432, 2384224, 2450016, 2515807, 2581343, 2647135, 2778462, 2843998, 2909789, 2975581, - 3106652, 3172444, 3238235, 3369306, 3435098, 3500633, 3631960, 3697752, 3828823, 3894614, - 3960150, 4091477, 4157012, 4288339, 4353874, 4485202, 4550737, 4682064, 4747599, 4878926, - 5009997, 5075789, 5206860, 5272651, 5403722, 5469513, 5600584, 5731911, 5797446, 5928773, - 5994309, 6125636, 6256707, 6322498, 6453569, 6519360, 6650431, 6781758, 6847294, 6978365, - 7109692, 7175227, 7306554, 7437625, 7568952, 7634488, 7765815, 7896886, 7962677, 8093748, - 8225076, 8356147, 8487474, 8553009, 8684337, 8815408, 8946735, 9077807, 9209134, 9340206, - 9405997, 9537069, 9668396, 9799468, 9930796, 10061868, 10193195, 10324267, 10455339, 10586667, - 10717740, 10849068, 10980140, 11111468, 11242541, 11373613, 11504942, 11636015, 11767343, - 11898416, 12029489, 12160818, 12291891, 12422964, 12488757, 12619830, 12750903, 12882232, - 13013306, 13144379, 13275708, 13341246, 13472319, 13603648, 13734722, 13800259, 13931589, - 14062662, 14193736, 14259530, 14390603, 14521677, 14587471, 14718545, 14784338, 14915412, - 14980950, 15112280, 15177818, 15309148, 15374686, 15440480, 15571554, 15637348, 15702887, - 15768681, 15834475, 15900013, 15965808, 16031602, 16097140, 16162935, 16228729, 16294267, - 16294526, 16360320, 16360322, 16426117, 16426375, 16492169, 16492172, 16557966, 16558224, - 16558483, 16558485, 16624279, 16624538, 16624796, 16624798, 16625056, 16625314, 16625573, - 16625575, 16625833, 16626091, 16626349, 16626351, 16626609, 16626868, 16626870, 16627128, - 16627386, 16627644, 16627646, 16627904, 16628162, 16628420, 16628423, 16628681, 16628939, - 16628941, 16563663, 16563921, 16564179, 16564182, 16564440, 16564698, 16564956, 16565215, - 16565217, 16565475, 16565733, 16500456, 16500458, 16500716, 16500975, 16501233, 16501491, - 16501494, 16436216, 16436474 - }; - public static final ScientificColourMaps6 BATLOW = - new ScientificColourMaps6(batlow, FAMILY.LINEAR); + private static final int[] batlow = { + 72025, 138073, 203866, 269914, 335706, 401755, 467547, 468059, 533851, 599900, 665692, 666204, 731996, 732509, + 798301, 798557, 864605, 864862, 865118, 931166, 931422, 931678, 997471, 997727, 998239, 998495, 1064287, + 1064543, 1064800, 1065056, 1130848, 1131104, 1131360, 1131616, 1197409, 1197665, 1197921, 1198177, 1263969, + 1264225, 1264481, 1330274, 1330530, 1330786, 1396578, 1396578, 1462370, 1462626, 1528418, 1528674, 1594466, + 1594722, 1660514, 1660770, 1726306, 1792098, 1792354, 1858146, 1923938, 1989730, 1989986, 2055521, 2121313, + 2187105, 2252897, 2318432, 2384224, 2450016, 2515807, 2581343, 2647135, 2778462, 2843998, 2909789, 2975581, + 3106652, 3172444, 3238235, 3369306, 3435098, 3500633, 3631960, 3697752, 3828823, 3894614, 3960150, 4091477, + 4157012, 4288339, 4353874, 4485202, 4550737, 4682064, 4747599, 4878926, 5009997, 5075789, 5206860, 5272651, + 5403722, 5469513, 5600584, 5731911, 5797446, 5928773, 5994309, 6125636, 6256707, 6322498, 6453569, 6519360, + 6650431, 6781758, 6847294, 6978365, 7109692, 7175227, 7306554, 7437625, 7568952, 7634488, 7765815, 7896886, + 7962677, 8093748, 8225076, 8356147, 8487474, 8553009, 8684337, 8815408, 8946735, 9077807, 9209134, 9340206, + 9405997, 9537069, 9668396, 9799468, 9930796, 10061868, 10193195, 10324267, 10455339, 10586667, 10717740, + 10849068, 10980140, 11111468, 11242541, 11373613, 11504942, 11636015, 11767343, 11898416, 12029489, 12160818, + 12291891, 12422964, 12488757, 12619830, 12750903, 12882232, 13013306, 13144379, 13275708, 13341246, 13472319, + 13603648, 13734722, 13800259, 13931589, 14062662, 14193736, 14259530, 14390603, 14521677, 14587471, 14718545, + 14784338, 14915412, 14980950, 15112280, 15177818, 15309148, 15374686, 15440480, 15571554, 15637348, 15702887, + 15768681, 15834475, 15900013, 15965808, 16031602, 16097140, 16162935, 16228729, 16294267, 16294526, 16360320, + 16360322, 16426117, 16426375, 16492169, 16492172, 16557966, 16558224, 16558483, 16558485, 16624279, 16624538, + 16624796, 16624798, 16625056, 16625314, 16625573, 16625575, 16625833, 16626091, 16626349, 16626351, 16626609, + 16626868, 16626870, 16627128, 16627386, 16627644, 16627646, 16627904, 16628162, 16628420, 16628423, 16628681, + 16628939, 16628941, 16563663, 16563921, 16564179, 16564182, 16564440, 16564698, 16564956, 16565215, 16565217, + 16565475, 16565733, 16500456, 16500458, 16500716, 16500975, 16501233, 16501491, 16501494, 16436216, 16436474 + }; + public static final ScientificColourMaps6 BATLOW = new ScientificColourMaps6(batlow, FAMILY.LINEAR); - private static final int[] berlin = { - 10399999, 10268926, 10137853, 10006524, 9809915, 9678842, 9547513, 9350903, 9219830, 9088757, - 8891892, 8760819, 8564210, 8432881, 8301808, 8105198, 7973869, 7777260, 7645931, 7449321, - 7317992, 7121382, 6990053, 6793443, 6662114, 6465248, 6333919, 6137309, 6005979, 5809113, - 5677783, 5546197, 5349331, 5218001, 5086671, 4955085, 4758218, 4626632, 4495302, 4429251, - 4297921, 4166334, 4100284, 3968697, 3902903, 3771316, 3705266, 3639215, 3573165, 3507370, - 3375784, 3309734, 3309219, 3243169, 3177118, 3111068, 3045017, 2979223, 2913172, 2912658, - 2846607, 2780557, 2714507, 2713992, 2648198, 2582147, 2581633, 2515583, 2449532, 2383482, - 2383224, 2317173, 2251123, 2250609, 2184558, 2118764, 2118250, 2052200, 1986149, 1985891, - 1919841, 1853791, 1853276, 1787482, 1721432, 1720918, 1655123, 1654609, 1588559, 1522765, - 1522251, 1456201, 1455943, 1389892, 1389378, 1323584, 1323070, 1257276, 1256762, 1190968, - 1190454, 1190196, 1124146, 1123888, 1123374, 1123116, 1122602, 1122344, 1056550, 1056037, - 1055779, 1121057, 1120800, 1120542, 1120284, 1119771, 1119513, 1119256, 1118999, 1184277, - 1184276, 1249554, 1314833, 1314576, 1379854, 1445389, 1510667, 1575946, 1641481, 1707016, - 1772295, 1837830, 1903365, 1968900, 2099972, 2165507, 2231298, 2296834, 2362370, 2427905, - 2493697, 2559233, 2624769, 2756097, 2821633, 2887168, 2952704, 3083776, 3149568, 3215104, - 3346176, 3411712, 3477504, 3608576, 3674112, 3739904, 3870976, 3936513, 4067841, 4133377, - 4264449, 4330241, 4461313, 4527105, 4658177, 4723970, 4855042, 4920834, 5051906, 5183235, - 5249027, 5380100, 5511429, 5642757, 5708550, 5839879, 5971208, 6102537, 6233866, 6365195, - 6496524, 6628110, 6824975, 6956304, 7087889, 7219219, 7350804, 7547670, 7679255, 7810841, - 7942171, 8073756, 8205342, 8402464, 8533794, 8665380, 8796966, 8928552, 9060138, 9191468, - 9323054, 9454640, 9586226, 9717812, 9849398, 9980985, 10112315, 10243901, 10375487, 10507073, - 10638660, 10770246, 10901832, 11033162, 11164748, 11296335, 11427921, 11559507, 11691093, - 11822680, 11954266, 12085852, 12217183, 12348769, 12480355, 12611941, 12743528, 12875114, - 13006700, 13138287, 13269873, 13401459, 13533046, 13664632, 13796218, 13993341, 14124927, - 14256514, 14388100, 14519686, 14651273, 14782859, 14914446, 15046032, 15177618, 15374741, - 15506327, 15637914, 15769500, 15901087, 16032673, 16164259, 16361382, 16492968, 16624555, - 16756141 - }; - public static final ScientificColourMaps6 BERLIN = - new ScientificColourMaps6(berlin, FAMILY.DIVERGENT); + private static final int[] berlin = { + 10399999, 10268926, 10137853, 10006524, 9809915, 9678842, 9547513, 9350903, 9219830, 9088757, 8891892, 8760819, + 8564210, 8432881, 8301808, 8105198, 7973869, 7777260, 7645931, 7449321, 7317992, 7121382, 6990053, 6793443, + 6662114, 6465248, 6333919, 6137309, 6005979, 5809113, 5677783, 5546197, 5349331, 5218001, 5086671, 4955085, + 4758218, 4626632, 4495302, 4429251, 4297921, 4166334, 4100284, 3968697, 3902903, 3771316, 3705266, 3639215, + 3573165, 3507370, 3375784, 3309734, 3309219, 3243169, 3177118, 3111068, 3045017, 2979223, 2913172, 2912658, + 2846607, 2780557, 2714507, 2713992, 2648198, 2582147, 2581633, 2515583, 2449532, 2383482, 2383224, 2317173, + 2251123, 2250609, 2184558, 2118764, 2118250, 2052200, 1986149, 1985891, 1919841, 1853791, 1853276, 1787482, + 1721432, 1720918, 1655123, 1654609, 1588559, 1522765, 1522251, 1456201, 1455943, 1389892, 1389378, 1323584, + 1323070, 1257276, 1256762, 1190968, 1190454, 1190196, 1124146, 1123888, 1123374, 1123116, 1122602, 1122344, + 1056550, 1056037, 1055779, 1121057, 1120800, 1120542, 1120284, 1119771, 1119513, 1119256, 1118999, 1184277, + 1184276, 1249554, 1314833, 1314576, 1379854, 1445389, 1510667, 1575946, 1641481, 1707016, 1772295, 1837830, + 1903365, 1968900, 2099972, 2165507, 2231298, 2296834, 2362370, 2427905, 2493697, 2559233, 2624769, 2756097, + 2821633, 2887168, 2952704, 3083776, 3149568, 3215104, 3346176, 3411712, 3477504, 3608576, 3674112, 3739904, + 3870976, 3936513, 4067841, 4133377, 4264449, 4330241, 4461313, 4527105, 4658177, 4723970, 4855042, 4920834, + 5051906, 5183235, 5249027, 5380100, 5511429, 5642757, 5708550, 5839879, 5971208, 6102537, 6233866, 6365195, + 6496524, 6628110, 6824975, 6956304, 7087889, 7219219, 7350804, 7547670, 7679255, 7810841, 7942171, 8073756, + 8205342, 8402464, 8533794, 8665380, 8796966, 8928552, 9060138, 9191468, 9323054, 9454640, 9586226, 9717812, + 9849398, 9980985, 10112315, 10243901, 10375487, 10507073, 10638660, 10770246, 10901832, 11033162, 11164748, + 11296335, 11427921, 11559507, 11691093, 11822680, 11954266, 12085852, 12217183, 12348769, 12480355, 12611941, + 12743528, 12875114, 13006700, 13138287, 13269873, 13401459, 13533046, 13664632, 13796218, 13993341, 14124927, + 14256514, 14388100, 14519686, 14651273, 14782859, 14914446, 15046032, 15177618, 15374741, 15506327, 15637914, + 15769500, 15901087, 16032673, 16164259, 16361382, 16492968, 16624555, 16756141 + }; + public static final ScientificColourMaps6 BERLIN = new ScientificColourMaps6(berlin, FAMILY.DIVERGENT); - private static final int[] brocO = { - 3616568, 3551033, 3551034, 3551035, 3551036, 3551293, 3551295, 3551296, 3551297, 3551555, - 3551556, 3551814, 3551815, 3552073, 3552074, 3552332, 3552590, 3552592, 3552849, 3553107, - 3553365, 3553623, 3619417, 3619675, 3619932, 3620190, 3620704, 3686498, 3686756, 3687270, - 3753064, 3753322, 3819372, 3819631, 3885681, 3886195, 3951989, 4018039, 4084089, 4084347, - 4150396, 4216446, 4282240, 4348290, 4414340, 4480390, 4546184, 4677770, 4743819, 4809869, - 4875919, 5007248, 5073298, 5204884, 5270933, 5402519, 5468313, 5599898, 5665948, 5797533, - 5929119, 5994912, 6126498, 6258083, 6389669, 6521254, 6587047, 6718633, 6850218, 6981803, - 7113133, 7244718, 7376303, 7507889, 7639218, 7770803, 7902388, 8033974, 8165303, 8296888, - 8428473, 8560058, 8691387, 8822973, 8954558, 9085887, 9217472, 9349057, 9545922, 9677507, - 9809092, 9940421, 10072006, 10203591, 10334920, 10466504, 10597833, 10729418, 10860747, - 10992332, 11123660, 11255245, 11386573, 11518158, 11649486, 11780815, 11912399, 12043728, - 12109520, 12241104, 12372432, 12438224, 12569552, 12700880, 12766672, 12898000, 12963791, - 13029327, 13095118, 13226446, 13291981, 13357772, 13357771, 13423563, 13489098, 13554632, - 13554631, 13620166, 13620165, 13620163, 13620162, 13620160, 13620158, 13619901, 13619899, - 13619641, 13553847, 13553845, 13488051, 13487793, 13421999, 13356205, 13290411, 13224617, - 13158822, 13093028, 13026978, 12961184, 12895389, 12763803, 12698009, 12566422, 12500628, - 12369042, 12303248, 12171661, 12105611, 11974281, 11842694, 11776644, 11645058, 11513727, - 11382141, 11250555, 11184505, 11052919, 10921332, 10790002, 10658416, 10526830, 10395244, - 10329194, 10197608, 10066022, 9934436, 9802850, 9671264, 9539934, 9408348, 9276762, 9145177, - 9013591, 8947541, 8815955, 8684369, 8552784, 8421198, 8289868, 8158283, 8092233, 7960648, - 7829062, 7697477, 7565891, 7500098, 7368513, 7236927, 7105342, 7039293, 6907963, 6776378, - 6710329, 6578744, 6447415, 6381366, 6249781, 6183988, 6052403, 5986610, 5855025, 5788976, - 5657647, 5591598, 5525806, 5394221, 5328429, 5262636, 5196587, 5065259, 4999211, 4933418, - 4867626, 4801834, 4735785, 4669993, 4604201, 4538409, 4472617, 4406825, 4341033, 4340777, - 4274985, 4209193, 4143401, 4143145, 4077610, 4077354, 4011562, 3945771, 3945771, 3879979, - 3879980, 3879724, 3814189, 3813934, 3748398, 3748143, 3748144, 3682352, 3682353, 3682354, - 3682355, 3616564, 3616565, 3616566, 3616567 - }; - public static final ScientificColourMaps6 BROCO = new ScientificColourMaps6(brocO, FAMILY.CYCLIC); + private static final int[] brocO = { + 3616568, 3551033, 3551034, 3551035, 3551036, 3551293, 3551295, 3551296, 3551297, 3551555, 3551556, 3551814, + 3551815, 3552073, 3552074, 3552332, 3552590, 3552592, 3552849, 3553107, 3553365, 3553623, 3619417, 3619675, + 3619932, 3620190, 3620704, 3686498, 3686756, 3687270, 3753064, 3753322, 3819372, 3819631, 3885681, 3886195, + 3951989, 4018039, 4084089, 4084347, 4150396, 4216446, 4282240, 4348290, 4414340, 4480390, 4546184, 4677770, + 4743819, 4809869, 4875919, 5007248, 5073298, 5204884, 5270933, 5402519, 5468313, 5599898, 5665948, 5797533, + 5929119, 5994912, 6126498, 6258083, 6389669, 6521254, 6587047, 6718633, 6850218, 6981803, 7113133, 7244718, + 7376303, 7507889, 7639218, 7770803, 7902388, 8033974, 8165303, 8296888, 8428473, 8560058, 8691387, 8822973, + 8954558, 9085887, 9217472, 9349057, 9545922, 9677507, 9809092, 9940421, 10072006, 10203591, 10334920, 10466504, + 10597833, 10729418, 10860747, 10992332, 11123660, 11255245, 11386573, 11518158, 11649486, 11780815, 11912399, + 12043728, 12109520, 12241104, 12372432, 12438224, 12569552, 12700880, 12766672, 12898000, 12963791, 13029327, + 13095118, 13226446, 13291981, 13357772, 13357771, 13423563, 13489098, 13554632, 13554631, 13620166, 13620165, + 13620163, 13620162, 13620160, 13620158, 13619901, 13619899, 13619641, 13553847, 13553845, 13488051, 13487793, + 13421999, 13356205, 13290411, 13224617, 13158822, 13093028, 13026978, 12961184, 12895389, 12763803, 12698009, + 12566422, 12500628, 12369042, 12303248, 12171661, 12105611, 11974281, 11842694, 11776644, 11645058, 11513727, + 11382141, 11250555, 11184505, 11052919, 10921332, 10790002, 10658416, 10526830, 10395244, 10329194, 10197608, + 10066022, 9934436, 9802850, 9671264, 9539934, 9408348, 9276762, 9145177, 9013591, 8947541, 8815955, 8684369, + 8552784, 8421198, 8289868, 8158283, 8092233, 7960648, 7829062, 7697477, 7565891, 7500098, 7368513, 7236927, + 7105342, 7039293, 6907963, 6776378, 6710329, 6578744, 6447415, 6381366, 6249781, 6183988, 6052403, 5986610, + 5855025, 5788976, 5657647, 5591598, 5525806, 5394221, 5328429, 5262636, 5196587, 5065259, 4999211, 4933418, + 4867626, 4801834, 4735785, 4669993, 4604201, 4538409, 4472617, 4406825, 4341033, 4340777, 4274985, 4209193, + 4143401, 4143145, 4077610, 4077354, 4011562, 3945771, 3945771, 3879979, 3879980, 3879724, 3814189, 3813934, + 3748398, 3748143, 3748144, 3682352, 3682353, 3682354, 3682355, 3616564, 3616565, 3616566, 3616567 + }; + public static final ScientificColourMaps6 BROCO = new ScientificColourMaps6(brocO, FAMILY.CYCLIC); - private static final int[] corkO = { - 4144698, 4144699, 4144700, 4144445, 4078910, 4078911, 4078912, 4078913, 4078914, 4078915, - 4078916, 4078917, 4078918, 4078919, 4078920, 4078921, 4078923, 4078924, 4079181, 4079183, - 4079440, 4079442, 4079699, 4079701, 4079958, 4079960, 4080218, 4080475, 4080733, 4080991, - 4146785, 4147042, 4147300, 4213094, 4213352, 4213610, 4279404, 4279918, 4345712, 4345970, - 4412020, 4477814, 4478328, 4544122, 4610172, 4675966, 4676480, 4742530, 4808324, 4874374, - 4940423, 5071753, 5137803, 5203853, 5269903, 5335697, 5467282, 5533332, 5664918, 5730711, - 5796761, 5928347, 6059932, 6125726, 6257311, 6323361, 6454946, 6586276, 6652325, 6783910, - 6915496, 6981289, 7112875, 7244460, 7376045, 7507374, 7573424, 7705009, 7836338, 7967923, - 8099508, 8165302, 8296887, 8428472, 8559801, 8691386, 8822971, 8888764, 9020349, 9151678, - 9283263, 9414592, 9480640, 9612225, 9743554, 9875139, 9940931, 10072516, 10138309, 10269637, - 10401222, 10467014, 10598599, 10664391, 10730183, 10861512, 10927560, 10993352, 11059144, - 11124936, 11190728, 11256520, 11322312, 11388104, 11453895, 11454151, 11519687, 11519942, - 11585733, 11585733, 11585988, 11585987, 11586242, 11586241, 11586240, 11586239, 11586238, - 11520957, 11520956, 11455163, 11389625, 11389624, 11324086, 11258549, 11192755, 11127218, - 11061424, 10995887, 10930093, 10864555, 10733226, 10667432, 10601638, 10470564, 10404771, - 10273441, 10207647, 10076317, 10010523, 9879193, 9813399, 9682069, 9550739, 9484945, 9353615, - 9222029, 9156235, 9024905, 8893575, 8827525, 8696195, 8564865, 8433279, 8367485, 8236155, - 8104569, 8038775, 7907189, 7775859, 7709809, 7578479, 7446892, 7380842, 7249512, 7117926, - 7051876, 6920546, 6854496, 6722910, 6657116, 6525530, 6459480, 6327894, 6262100, 6196050, - 6064464, 5998414, 5932620, 5866570, 5734985, 5668935, 5603141, 5537092, 5471042, 5404993, - 5339199, 5273150, 5207100, 5206843, 5140794, 5074744, 5008951, 5008438, 4942645, 4876596, - 4876339, 4810290, 4810034, 4744241, 4743728, 4677936, 4677679, 4611631, 4611374, 4611118, - 4545325, 4544813, 4544557, 4544301, 4478508, 4478252, 4477996, 4477740, 4411948, 4411692, - 4411436, 4411180, 4411180, 4345389, 4345133, 4344877, 4344621, 4344622, 4344366, 4344110, - 4278319, 4278319, 4278063, 4278064, 4277808, 4277553, 4277553, 4277298, 4211762, 4211507, - 4211507, 4211252, 4211252, 4210997, 4210998, 4210742, 4145207, 4144952, 4144952, 4144953, - 4144698 - }; - public static final ScientificColourMaps6 CORKO = new ScientificColourMaps6(corkO, FAMILY.CYCLIC); + private static final int[] corkO = { + 4144698, 4144699, 4144700, 4144445, 4078910, 4078911, 4078912, 4078913, 4078914, 4078915, 4078916, 4078917, + 4078918, 4078919, 4078920, 4078921, 4078923, 4078924, 4079181, 4079183, 4079440, 4079442, 4079699, 4079701, + 4079958, 4079960, 4080218, 4080475, 4080733, 4080991, 4146785, 4147042, 4147300, 4213094, 4213352, 4213610, + 4279404, 4279918, 4345712, 4345970, 4412020, 4477814, 4478328, 4544122, 4610172, 4675966, 4676480, 4742530, + 4808324, 4874374, 4940423, 5071753, 5137803, 5203853, 5269903, 5335697, 5467282, 5533332, 5664918, 5730711, + 5796761, 5928347, 6059932, 6125726, 6257311, 6323361, 6454946, 6586276, 6652325, 6783910, 6915496, 6981289, + 7112875, 7244460, 7376045, 7507374, 7573424, 7705009, 7836338, 7967923, 8099508, 8165302, 8296887, 8428472, + 8559801, 8691386, 8822971, 8888764, 9020349, 9151678, 9283263, 9414592, 9480640, 9612225, 9743554, 9875139, + 9940931, 10072516, 10138309, 10269637, 10401222, 10467014, 10598599, 10664391, 10730183, 10861512, 10927560, + 10993352, 11059144, 11124936, 11190728, 11256520, 11322312, 11388104, 11453895, 11454151, 11519687, 11519942, + 11585733, 11585733, 11585988, 11585987, 11586242, 11586241, 11586240, 11586239, 11586238, 11520957, 11520956, + 11455163, 11389625, 11389624, 11324086, 11258549, 11192755, 11127218, 11061424, 10995887, 10930093, 10864555, + 10733226, 10667432, 10601638, 10470564, 10404771, 10273441, 10207647, 10076317, 10010523, 9879193, 9813399, + 9682069, 9550739, 9484945, 9353615, 9222029, 9156235, 9024905, 8893575, 8827525, 8696195, 8564865, 8433279, + 8367485, 8236155, 8104569, 8038775, 7907189, 7775859, 7709809, 7578479, 7446892, 7380842, 7249512, 7117926, + 7051876, 6920546, 6854496, 6722910, 6657116, 6525530, 6459480, 6327894, 6262100, 6196050, 6064464, 5998414, + 5932620, 5866570, 5734985, 5668935, 5603141, 5537092, 5471042, 5404993, 5339199, 5273150, 5207100, 5206843, + 5140794, 5074744, 5008951, 5008438, 4942645, 4876596, 4876339, 4810290, 4810034, 4744241, 4743728, 4677936, + 4677679, 4611631, 4611374, 4611118, 4545325, 4544813, 4544557, 4544301, 4478508, 4478252, 4477996, 4477740, + 4411948, 4411692, 4411436, 4411180, 4411180, 4345389, 4345133, 4344877, 4344621, 4344622, 4344366, 4344110, + 4278319, 4278319, 4278063, 4278064, 4277808, 4277553, 4277553, 4277298, 4211762, 4211507, 4211507, 4211252, + 4211252, 4210997, 4210998, 4210742, 4145207, 4144952, 4144952, 4144953, 4144698 + }; + public static final ScientificColourMaps6 CORKO = new ScientificColourMaps6(corkO, FAMILY.CYCLIC); - private static final int[] oleron = { - 1713753, 1779803, 1911132, 1977182, 2108511, 2174561, 2305890, 2371940, 2503269, 2634855, - 2700648, 2832234, 2898027, 3029613, 3095407, 3226992, 3293042, 3424371, 3490421, 3621750, - 3753336, 3819386, 3950715, 4016765, 4148094, 4279680, 4345730, 4477059, 4543109, 4674695, - 4806024, 4872074, 5003660, 5134989, 5201039, 5332625, 5463954, 5530004, 5661590, 5792919, - 5858969, 5990555, 6121884, 6187934, 6319520, 6451106, 6582435, 6648485, 6780071, 6911401, - 7042986, 7109036, 7240622, 7372208, 7503537, 7569587, 7701173, 7832759, 7964088, 8030138, - 8161724, 8293310, 8424896, 8556225, 8622275, 8753861, 8885447, 9017033, 9148362, 9279948, - 9345998, 9477584, 9609170, 9740755, 9872085, 10003671, 10069721, 10201307, 10332892, 10464478, - 10595808, 10727393, 10793443, 10925029, 11056614, 11188200, 11319529, 11385578, 11517164, - 11648493, 11714542, 11846127, 11977456, 12043505, 12175090, 12240883, 12372467, 12438260, - 12569588, 12635637, 12766965, 12833014, 12898806, 13030135, 13096183, 13161976, 13293304, - 13359352, 13425145, 13556473, 13622521, 13688314, 13819642, 13885690, 13951483, 14082811, - 14148859, 14214652, 14345980, 14412028, 14477821, 14609149, 14675197, 14740990, 14872318, - 14938366, 15004159, 15135487, 1723392, 1920256, 2051584, 2248448, 2445056, 2576384, 2773248, - 2904320, 3101184, 3232512, 3363840, 3494912, 3691776, 3823104, 3954176, 4085504, 4216576, - 4347904, 4479232, 4610304, 4807169, 4938497, 5069825, 5200898, 5332226, 5463554, 5594883, - 5726212, 5922821, 6054150, 6185479, 6316809, 6513674, 6645260, 6776590, 6973456, 7104786, - 7236116, 7433238, 7564568, 7695898, 7893021, 8024351, 8155681, 8287267, 8484133, 8615464, - 8747050, 8878380, 9075502, 9206833, 9338163, 9469749, 9601079, 9732666, 9929532, 10060862, - 10192448, 10323779, 10455365, 10586695, 10718281, 10915148, 11046734, 11178064, 11309651, - 11440981, 11638103, 11769689, 11901020, 12032606, 12229728, 12361059, 12492645, 12689767, - 12821354, 12952684, 13084270, 13281393, 13412979, 13544565, 13741432, 13873018, 14004604, - 14136191, 14267777, 14464644, 14596230, 14727817, 14859403, 14990990, 15057040, 15188627, - 15320213, 15386008, 15517594, 15583645, 15649695, 15781282, 15847076, 15913127, 15913641, - 15979691, 16045486, 16111536, 16112050, 16177845, 16178359, 16244409, 16244668, 16245182, - 16310976, 16311491, 16312005, 16377799, 16378313, 16378572, 16444622, 16445136, 16445395, - 16511445, 16511959, 16512218, 16512732, 16578782, 16579041, 16579555, 16645606 - }; - public static final ScientificColourMaps6 OLERON = - new ScientificColourMaps6(oleron, FAMILY.LINEAR); + private static final int[] oleron = { + 1713753, 1779803, 1911132, 1977182, 2108511, 2174561, 2305890, 2371940, 2503269, 2634855, 2700648, 2832234, + 2898027, 3029613, 3095407, 3226992, 3293042, 3424371, 3490421, 3621750, 3753336, 3819386, 3950715, 4016765, + 4148094, 4279680, 4345730, 4477059, 4543109, 4674695, 4806024, 4872074, 5003660, 5134989, 5201039, 5332625, + 5463954, 5530004, 5661590, 5792919, 5858969, 5990555, 6121884, 6187934, 6319520, 6451106, 6582435, 6648485, + 6780071, 6911401, 7042986, 7109036, 7240622, 7372208, 7503537, 7569587, 7701173, 7832759, 7964088, 8030138, + 8161724, 8293310, 8424896, 8556225, 8622275, 8753861, 8885447, 9017033, 9148362, 9279948, 9345998, 9477584, + 9609170, 9740755, 9872085, 10003671, 10069721, 10201307, 10332892, 10464478, 10595808, 10727393, 10793443, + 10925029, 11056614, 11188200, 11319529, 11385578, 11517164, 11648493, 11714542, 11846127, 11977456, 12043505, + 12175090, 12240883, 12372467, 12438260, 12569588, 12635637, 12766965, 12833014, 12898806, 13030135, 13096183, + 13161976, 13293304, 13359352, 13425145, 13556473, 13622521, 13688314, 13819642, 13885690, 13951483, 14082811, + 14148859, 14214652, 14345980, 14412028, 14477821, 14609149, 14675197, 14740990, 14872318, 14938366, 15004159, + 15135487, 1723392, 1920256, 2051584, 2248448, 2445056, 2576384, 2773248, 2904320, 3101184, 3232512, 3363840, + 3494912, 3691776, 3823104, 3954176, 4085504, 4216576, 4347904, 4479232, 4610304, 4807169, 4938497, 5069825, + 5200898, 5332226, 5463554, 5594883, 5726212, 5922821, 6054150, 6185479, 6316809, 6513674, 6645260, 6776590, + 6973456, 7104786, 7236116, 7433238, 7564568, 7695898, 7893021, 8024351, 8155681, 8287267, 8484133, 8615464, + 8747050, 8878380, 9075502, 9206833, 9338163, 9469749, 9601079, 9732666, 9929532, 10060862, 10192448, 10323779, + 10455365, 10586695, 10718281, 10915148, 11046734, 11178064, 11309651, 11440981, 11638103, 11769689, 11901020, + 12032606, 12229728, 12361059, 12492645, 12689767, 12821354, 12952684, 13084270, 13281393, 13412979, 13544565, + 13741432, 13873018, 14004604, 14136191, 14267777, 14464644, 14596230, 14727817, 14859403, 14990990, 15057040, + 15188627, 15320213, 15386008, 15517594, 15583645, 15649695, 15781282, 15847076, 15913127, 15913641, 15979691, + 16045486, 16111536, 16112050, 16177845, 16178359, 16244409, 16244668, 16245182, 16310976, 16311491, 16312005, + 16377799, 16378313, 16378572, 16444622, 16445136, 16445395, 16511445, 16511959, 16512218, 16512732, 16578782, + 16579041, 16579555, 16645606 + }; + public static final ScientificColourMaps6 OLERON = new ScientificColourMaps6(oleron, FAMILY.LINEAR); - private static final int[] roma = { - 8329472, 8395777, 8462082, 8528387, 8594692, 8660741, 8792581, 8858630, 8924935, 8990984, - 9057032, 9123081, 9189386, 9255435, 9387020, 9453069, 9519118, 9585167, 9651216, 9717520, - 9783569, 9849618, 9915667, 9981716, 10047765, 10113814, 10179862, 10245911, 10311960, 10378009, - 10444058, 10510107, 10576156, 10642204, 10708253, 10774302, 10840351, 10906400, 10972449, - 11038497, 11104546, 11170339, 11236388, 11302437, 11368486, 11368999, 11435047, 11501096, - 11567145, 11633194, 11699243, 11765292, 11831341, 11897390, 11963439, 12029744, 12095793, - 12161843, 12227892, 12293941, 12359990, 12426040, 12557625, 12623674, 12689980, 12756030, - 12822079, 12888129, 12954435, 13020485, 13152071, 13218121, 13284427, 13350477, 13482063, - 13548370, 13614420, 13680471, 13746521, 13878108, 13944415, 14010465, 14076516, 14142567, - 14208618, 14274413, 14340463, 14406514, 14472565, 14538360, 14604411, 14604670, 14670720, - 14736515, 14736774, 14802569, 14802827, 14868622, 14868880, 14869139, 14934933, 14935192, - 14935450, 14935453, 14935711, 14935969, 14870435, 14870694, 14870696, 14805418, 14805420, - 14740142, 14740144, 14674610, 14609076, 14543798, 14478263, 14412729, 14347195, 14281660, - 14150590, 14085312, 13954241, 13888707, 13757636, 13691845, 13560775, 13429704, 13298633, - 13167562, 13036491, 12905164, 12774093, 12577486, 12446159, 12315088, 12118225, 11987154, - 11790291, 11658963, 11462356, 11265492, 11134165, 10937301, 10740694, 10543830, 10412503, - 10215639, 10018775, 9821655, 9624791, 9427928, 9296600, 9099480, 8902616, 8705752, 8574168, - 8377303, 8180183, 8048855, 7851735, 7720150, 7523286, 7391702, 7260117, 7063253, 6931669, - 6800084, 6668756, 6537171, 6405587, 6339538, 6208209, 6076625, 6010576, 5878992, 5812943, - 5747150, 5615566, 5549517, 5483468, 5417676, 5286091, 5220042, 5153993, 5088201, 5022152, - 4956103, 4890055, 4889798, 4823749, 4757701, 4691652, 4625859, 4559810, 4559298, 4493505, - 4427456, 4361407, 4360895, 4295102, 4229053, 4228541, 4162748, 4096699, 4030907, 4030394, - 3964345, 3898552, 3898040, 3831991, 3766198, 3765686, 3699893, 3699380, 3633332, 3567539, - 3567026, 3501233, 3435185, 3434672, 3368879, 3302831, 3302574, 3236525, 3170733, 3170220, - 3104427, 3103915, 3037866, 2972073, 2971561, 2905768, 2839719, 2839463, 2773414, 2707621, - 2707109, 2641316, 2575267, 2575011, 2508962, 2443169, 2442657, 2376864, 2310816, 2245023, - 2244510, 2178718, 2112669, 2046876, 1980828, 1915035, 1848986, 1783194, 1717145 - }; - public static final ScientificColourMaps6 ROMA = - new ScientificColourMaps6(roma, FAMILY.DIVERGENT); + private static final int[] roma = { + 8329472, 8395777, 8462082, 8528387, 8594692, 8660741, 8792581, 8858630, 8924935, 8990984, 9057032, 9123081, + 9189386, 9255435, 9387020, 9453069, 9519118, 9585167, 9651216, 9717520, 9783569, 9849618, 9915667, 9981716, + 10047765, 10113814, 10179862, 10245911, 10311960, 10378009, 10444058, 10510107, 10576156, 10642204, 10708253, + 10774302, 10840351, 10906400, 10972449, 11038497, 11104546, 11170339, 11236388, 11302437, 11368486, 11368999, + 11435047, 11501096, 11567145, 11633194, 11699243, 11765292, 11831341, 11897390, 11963439, 12029744, 12095793, + 12161843, 12227892, 12293941, 12359990, 12426040, 12557625, 12623674, 12689980, 12756030, 12822079, 12888129, + 12954435, 13020485, 13152071, 13218121, 13284427, 13350477, 13482063, 13548370, 13614420, 13680471, 13746521, + 13878108, 13944415, 14010465, 14076516, 14142567, 14208618, 14274413, 14340463, 14406514, 14472565, 14538360, + 14604411, 14604670, 14670720, 14736515, 14736774, 14802569, 14802827, 14868622, 14868880, 14869139, 14934933, + 14935192, 14935450, 14935453, 14935711, 14935969, 14870435, 14870694, 14870696, 14805418, 14805420, 14740142, + 14740144, 14674610, 14609076, 14543798, 14478263, 14412729, 14347195, 14281660, 14150590, 14085312, 13954241, + 13888707, 13757636, 13691845, 13560775, 13429704, 13298633, 13167562, 13036491, 12905164, 12774093, 12577486, + 12446159, 12315088, 12118225, 11987154, 11790291, 11658963, 11462356, 11265492, 11134165, 10937301, 10740694, + 10543830, 10412503, 10215639, 10018775, 9821655, 9624791, 9427928, 9296600, 9099480, 8902616, 8705752, 8574168, + 8377303, 8180183, 8048855, 7851735, 7720150, 7523286, 7391702, 7260117, 7063253, 6931669, 6800084, 6668756, + 6537171, 6405587, 6339538, 6208209, 6076625, 6010576, 5878992, 5812943, 5747150, 5615566, 5549517, 5483468, + 5417676, 5286091, 5220042, 5153993, 5088201, 5022152, 4956103, 4890055, 4889798, 4823749, 4757701, 4691652, + 4625859, 4559810, 4559298, 4493505, 4427456, 4361407, 4360895, 4295102, 4229053, 4228541, 4162748, 4096699, + 4030907, 4030394, 3964345, 3898552, 3898040, 3831991, 3766198, 3765686, 3699893, 3699380, 3633332, 3567539, + 3567026, 3501233, 3435185, 3434672, 3368879, 3302831, 3302574, 3236525, 3170733, 3170220, 3104427, 3103915, + 3037866, 2972073, 2971561, 2905768, 2839719, 2839463, 2773414, 2707621, 2707109, 2641316, 2575267, 2575011, + 2508962, 2443169, 2442657, 2376864, 2310816, 2245023, 2244510, 2178718, 2112669, 2046876, 1980828, 1915035, + 1848986, 1783194, 1717145 + }; + public static final ScientificColourMaps6 ROMA = new ScientificColourMaps6(roma, FAMILY.DIVERGENT); - private static final int[] romaO = { - 7551319, 7616854, 7682388, 7682131, 7747665, 7813200, 7813199, 7878733, 7944268, 7944267, - 8009801, 8075336, 8140871, 8141126, 8206661, 8272195, 8272194, 8337985, 8403520, 8469311, - 8469310, 8535101, 8600636, 8666427, 8666426, 8732217, 8798008, 8863799, 8863799, 8929590, - 8995381, 9061172, 9126963, 9192755, 9193010, 9258801, 9324593, 9390384, 9456176, 9522223, - 9588015, 9653806, 9719854, 9720109, 9785901, 9851949, 9917740, 9983788, 10049580, 10115628, - 10181420, 10247467, 10313515, 10379307, 10445355, 10511404, 10642988, 10708780, 10774828, - 10840877, 10906925, 10972973, 11039022, 11105070, 11171119, 11237168, 11368752, 11434801, - 11500850, 11566899, 11632948, 11698997, 11830838, 11896887, 11962937, 12028986, 12095035, - 12161341, 12292927, 12358976, 12425282, 12491332, 12557382, 12689223, 12755273, 12821323, - 12887374, 12953680, 13019730, 13151316, 13217623, 13283673, 13349724, 13415774, 13482081, - 13548131, 13614182, 13614696, 13680747, 13746798, 13812848, 13813363, 13879414, 13945208, - 13945723, 13946238, 14012033, 14012547, 14012806, 14078856, 14079115, 14079374, 14079888, - 14080147, 14014869, 14015128, 14015386, 13950108, 13950367, 13884833, 13885091, 13819813, - 13754279, 13689001, 13623467, 13557933, 13492655, 13427121, 13361587, 13296053, 13164982, - 13099448, 12968377, 12902843, 12771772, 12706238, 12575167, 12443840, 12312770, 12181443, - 12115908, 11984581, 11853510, 11656647, 11525320, 11394248, 11262921, 11131594, 11000267, - 10803403, 10672076, 10540748, 10343885, 10212557, 10081230, 9884110, 9752782, 9621454, 9424591, - 9293007, 9161679, 8964559, 8833231, 8701647, 8504783, 8373199, 8241870, 8110286, 7913166, - 7781838, 7650253, 7518669, 7387341, 7255756, 7124172, 6992587, 6861259, 6795210, 6663626, - 6532041, 6465993, 6334664, 6268615, 6137031, 6070982, 5939397, 5873348, 5807300, 5741507, - 5675458, 5609409, 5543360, 5477311, 5411262, 5410749, 5344700, 5278651, 5278394, 5212345, - 5211832, 5211319, 5210806, 5144756, 5144243, 5143730, 5143216, 5208239, 5207726, 5207468, - 5206955, 5271977, 5271464, 5336486, 5335972, 5400995, 5400481, 5465759, 5530782, 5530268, - 5595290, 5660568, 5725590, 5725076, 5790099, 5855377, 5920399, 5985677, 6050699, 6050441, - 6115463, 6180741, 6245763, 6311041, 6376319, 6441341, 6506619, 6506361, 6571639, 6636918, - 6702196, 6767474, 6832752, 6832494, 6897772, 6963051, 7028585, 7093863, 7093605, 7159140, - 7224418, 7289696, 7289695, 7354973, 7420508, 7486042, 7485785 - }; - public static final ScientificColourMaps6 ROMAO = new ScientificColourMaps6(romaO, FAMILY.CYCLIC); + private static final int[] romaO = { + 7551319, 7616854, 7682388, 7682131, 7747665, 7813200, 7813199, 7878733, 7944268, 7944267, 8009801, 8075336, + 8140871, 8141126, 8206661, 8272195, 8272194, 8337985, 8403520, 8469311, 8469310, 8535101, 8600636, 8666427, + 8666426, 8732217, 8798008, 8863799, 8863799, 8929590, 8995381, 9061172, 9126963, 9192755, 9193010, 9258801, + 9324593, 9390384, 9456176, 9522223, 9588015, 9653806, 9719854, 9720109, 9785901, 9851949, 9917740, 9983788, + 10049580, 10115628, 10181420, 10247467, 10313515, 10379307, 10445355, 10511404, 10642988, 10708780, 10774828, + 10840877, 10906925, 10972973, 11039022, 11105070, 11171119, 11237168, 11368752, 11434801, 11500850, 11566899, + 11632948, 11698997, 11830838, 11896887, 11962937, 12028986, 12095035, 12161341, 12292927, 12358976, 12425282, + 12491332, 12557382, 12689223, 12755273, 12821323, 12887374, 12953680, 13019730, 13151316, 13217623, 13283673, + 13349724, 13415774, 13482081, 13548131, 13614182, 13614696, 13680747, 13746798, 13812848, 13813363, 13879414, + 13945208, 13945723, 13946238, 14012033, 14012547, 14012806, 14078856, 14079115, 14079374, 14079888, 14080147, + 14014869, 14015128, 14015386, 13950108, 13950367, 13884833, 13885091, 13819813, 13754279, 13689001, 13623467, + 13557933, 13492655, 13427121, 13361587, 13296053, 13164982, 13099448, 12968377, 12902843, 12771772, 12706238, + 12575167, 12443840, 12312770, 12181443, 12115908, 11984581, 11853510, 11656647, 11525320, 11394248, 11262921, + 11131594, 11000267, 10803403, 10672076, 10540748, 10343885, 10212557, 10081230, 9884110, 9752782, 9621454, + 9424591, 9293007, 9161679, 8964559, 8833231, 8701647, 8504783, 8373199, 8241870, 8110286, 7913166, 7781838, + 7650253, 7518669, 7387341, 7255756, 7124172, 6992587, 6861259, 6795210, 6663626, 6532041, 6465993, 6334664, + 6268615, 6137031, 6070982, 5939397, 5873348, 5807300, 5741507, 5675458, 5609409, 5543360, 5477311, 5411262, + 5410749, 5344700, 5278651, 5278394, 5212345, 5211832, 5211319, 5210806, 5144756, 5144243, 5143730, 5143216, + 5208239, 5207726, 5207468, 5206955, 5271977, 5271464, 5336486, 5335972, 5400995, 5400481, 5465759, 5530782, + 5530268, 5595290, 5660568, 5725590, 5725076, 5790099, 5855377, 5920399, 5985677, 6050699, 6050441, 6115463, + 6180741, 6245763, 6311041, 6376319, 6441341, 6506619, 6506361, 6571639, 6636918, 6702196, 6767474, 6832752, + 6832494, 6897772, 6963051, 7028585, 7093863, 7093605, 7159140, 7224418, 7289696, 7289695, 7354973, 7420508, + 7486042, 7485785 + }; + public static final ScientificColourMaps6 ROMAO = new ScientificColourMaps6(romaO, FAMILY.CYCLIC); - private static final int[] vikO = { - 5184061, 5118526, 5118783, 5053249, 4987970, 4922691, 4922692, 4857414, 4792135, 4726857, - 4727114, 4661579, 4596301, 4531022, 4531280, 4466001, 4400723, 4400980, 4335702, 4270424, - 4205145, 4140123, 4140381, 4075102, 4009824, 4010338, 3945059, 3879781, 3814759, 3815016, - 3749994, 3684716, 3685230, 3619951, 3554929, 3555187, 3490164, 3490678, 3425400, 3425914, - 3426427, 3426685, 3361663, 3362176, 3362690, 3363204, 3363461, 3429511, 3430025, 3430538, - 3496588, 3497101, 3563151, 3628944, 3694994, 3761044, 3827093, 3893143, 3959192, 4025242, - 4156827, 4222876, 4354462, 4420511, 4552097, 4683682, 4815268, 4946853, 5078438, 5210024, - 5341609, 5473195, 5604780, 5736365, 5933487, 6065072, 6196657, 6393778, 6525364, 6722485, - 6854070, 7051191, 7182520, 7379642, 7511227, 7708348, 7905469, 8037054, 8234175, 8431040, - 8628161, 8759746, 8956610, 9153731, 9350852, 9482181, 9679301, 9876166, 10073286, 10204615, - 10401479, 10598600, 10795464, 10926792, 11123656, 11254984, 11451848, 11583176, 11780040, - 11911368, 12107975, 12239303, 12370374, 12501702, 12632773, 12763844, 12894915, 13026242, - 13157313, 13222592, 13353663, 13484734, 13550268, 13615547, 13746617, 13811896, 13877430, - 13942709, 14007987, 14007729, 14073263, 14138541, 14138284, 14203562, 14203048, 14268326, - 14268068, 14267809, 14267551, 14267037, 14266779, 14266265, 14266007, 14265492, 14265234, - 14264720, 14198925, 14198411, 14198153, 14132103, 14131588, 14065794, 13999744, 13999229, - 13933179, 13932665, 13866870, 13800820, 13734770, 13668719, 13668205, 13602155, 13536104, - 13470310, 13404260, 13338209, 13272159, 13140573, 13074523, 13008472, 12942422, 12876116, - 12744530, 12678479, 12612429, 12480843, 12414793, 12283207, 12216901, 12085315, 12019265, - 11887679, 11756093, 11689787, 11558201, 11426615, 11295029, 11163187, 11097138, 10965552, - 10833967, 10702125, 10570540, 10438954, 10307369, 10175784, 10044199, 9912614, 9780773, 9649188, - 9517859, 9386274, 9254689, 9188641, 9057056, 8925472, 8794143, 8662559, 8530975, 8465182, - 8333598, 8202270, 8136478, 8004894, 7939102, 7807774, 7741983, 7610655, 7544863, 7479071, - 7347744, 7281952, 7216160, 7084833, 7019041, 6953506, 6887714, 6822179, 6756387, 6690852, - 6625061, 6559525, 6493734, 6428199, 6362663, 6297128, 6231593, 6165802, 6100267, 6034731, - 5969196, 5903661, 5903662, 5838127, 5772848, 5707313, 5641778, 5641779, 5576244, 5510965, - 5445430, 5445431, 5380152, 5314617, 5249338, 5249340 - }; - public static final ScientificColourMaps6 VIKO = new ScientificColourMaps6(vikO, FAMILY.CYCLIC); + private static final int[] vikO = { + 5184061, 5118526, 5118783, 5053249, 4987970, 4922691, 4922692, 4857414, 4792135, 4726857, 4727114, 4661579, + 4596301, 4531022, 4531280, 4466001, 4400723, 4400980, 4335702, 4270424, 4205145, 4140123, 4140381, 4075102, + 4009824, 4010338, 3945059, 3879781, 3814759, 3815016, 3749994, 3684716, 3685230, 3619951, 3554929, 3555187, + 3490164, 3490678, 3425400, 3425914, 3426427, 3426685, 3361663, 3362176, 3362690, 3363204, 3363461, 3429511, + 3430025, 3430538, 3496588, 3497101, 3563151, 3628944, 3694994, 3761044, 3827093, 3893143, 3959192, 4025242, + 4156827, 4222876, 4354462, 4420511, 4552097, 4683682, 4815268, 4946853, 5078438, 5210024, 5341609, 5473195, + 5604780, 5736365, 5933487, 6065072, 6196657, 6393778, 6525364, 6722485, 6854070, 7051191, 7182520, 7379642, + 7511227, 7708348, 7905469, 8037054, 8234175, 8431040, 8628161, 8759746, 8956610, 9153731, 9350852, 9482181, + 9679301, 9876166, 10073286, 10204615, 10401479, 10598600, 10795464, 10926792, 11123656, 11254984, 11451848, + 11583176, 11780040, 11911368, 12107975, 12239303, 12370374, 12501702, 12632773, 12763844, 12894915, 13026242, + 13157313, 13222592, 13353663, 13484734, 13550268, 13615547, 13746617, 13811896, 13877430, 13942709, 14007987, + 14007729, 14073263, 14138541, 14138284, 14203562, 14203048, 14268326, 14268068, 14267809, 14267551, 14267037, + 14266779, 14266265, 14266007, 14265492, 14265234, 14264720, 14198925, 14198411, 14198153, 14132103, 14131588, + 14065794, 13999744, 13999229, 13933179, 13932665, 13866870, 13800820, 13734770, 13668719, 13668205, 13602155, + 13536104, 13470310, 13404260, 13338209, 13272159, 13140573, 13074523, 13008472, 12942422, 12876116, 12744530, + 12678479, 12612429, 12480843, 12414793, 12283207, 12216901, 12085315, 12019265, 11887679, 11756093, 11689787, + 11558201, 11426615, 11295029, 11163187, 11097138, 10965552, 10833967, 10702125, 10570540, 10438954, 10307369, + 10175784, 10044199, 9912614, 9780773, 9649188, 9517859, 9386274, 9254689, 9188641, 9057056, 8925472, 8794143, + 8662559, 8530975, 8465182, 8333598, 8202270, 8136478, 8004894, 7939102, 7807774, 7741983, 7610655, 7544863, + 7479071, 7347744, 7281952, 7216160, 7084833, 7019041, 6953506, 6887714, 6822179, 6756387, 6690852, 6625061, + 6559525, 6493734, 6428199, 6362663, 6297128, 6231593, 6165802, 6100267, 6034731, 5969196, 5903661, 5903662, + 5838127, 5772848, 5707313, 5641778, 5641779, 5576244, 5510965, 5445430, 5445431, 5380152, 5314617, 5249338, + 5249340 + }; + public static final ScientificColourMaps6 VIKO = new ScientificColourMaps6(vikO, FAMILY.CYCLIC); - private final int[] colors; + private final int[] colors; - private ScientificColourMaps6(int[] colors, ColorTable.FAMILY family) { - super(family); - this.colors = colors; - } + private ScientificColourMaps6(int[] colors, ColorTable.FAMILY family) { + super(family); + this.colors = colors; + } - @Override - public List getColors() { - List colors = new ArrayList<>(); - for (int color : this.colors) colors.add(new Color(color)); - return colors; - } + @Override + public List getColors() { + List colors = new ArrayList<>(); + for (int color : this.colors) colors.add(new Color(color)); + return colors; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/config/PlotConfig.java b/src/main/java/terrasaur/utils/saaPlotLib/config/PlotConfig.java index f79acee..6044822 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/config/PlotConfig.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/config/PlotConfig.java @@ -39,148 +39,148 @@ import org.immutables.value.Value; @Value.Immutable public abstract class PlotConfig { - /** Font used for axes and color bar */ - @Value.Default - public Font axisFont() { - return new Font("Times New Roman", Font.BOLD, 16); - } + /** Font used for axes and color bar */ + @Value.Default + public Font axisFont() { + return new Font("Times New Roman", Font.BOLD, 16); + } - @Value.Default - public Color backgroundColor() { - return Color.WHITE; - } + @Value.Default + public Color backgroundColor() { + return Color.WHITE; + } - @Value.Default - public int bottomMargin() { - return 80; - } + @Value.Default + public int bottomMargin() { + return 80; + } - /** - * @return y coordinate of the bottom plot edge. (0, 0) is at the upper left. This is {@link - * #topMargin()}+{@link #height()}. - */ - public int getBottomPlotEdge() { - return topMargin() + height(); - } + /** + * @return y coordinate of the bottom plot edge. (0, 0) is at the upper left. This is {@link + * #topMargin()}+{@link #height()}. + */ + public int getBottomPlotEdge() { + return topMargin() + height(); + } - /** - * @return x coordinate of the left plot edge. (0, 0) is at the upper left. This is the same - * as{@link #leftMargin()}. - */ - public int getLeftPlotEdge() { - return leftMargin(); - } + /** + * @return x coordinate of the left plot edge. (0, 0) is at the upper left. This is the same + * as{@link #leftMargin()}. + */ + public int getLeftPlotEdge() { + return leftMargin(); + } - /** - * @return x coordinate of the right plot edge. (0, 0) is at the upper left. This is the same - * as{@link #leftMargin()} + {@link #width()}. - */ - public int getRightPlotEdge() { - return leftMargin() + width(); - } + /** + * @return x coordinate of the right plot edge. (0, 0) is at the upper left. This is the same + * as{@link #leftMargin()} + {@link #width()}. + */ + public int getRightPlotEdge() { + return leftMargin() + width(); + } - /** - * @return y coordinate of the top plot edge. (0, 0) is at the upper left. This is the same as - * {@link #topMargin()}. - */ - public int getTopPlotEdge() { - return topMargin(); - } + /** + * @return y coordinate of the top plot edge. (0, 0) is at the upper left. This is the same as + * {@link #topMargin()}. + */ + public int getTopPlotEdge() { + return topMargin(); + } - @Value.Default - public Color gridColor() { - return Color.lightGray; - } + @Value.Default + public Color gridColor() { + return Color.lightGray; + } - @Value.Default - public Font gridFont() { - return new Font("Helvetica", Font.BOLD, 12); - } + @Value.Default + public Font gridFont() { + return new Font("Helvetica", Font.BOLD, 12); + } - /** - * @return height of the plot area. Does not include margins. - */ - @Value.Default - public int height() { - return 900; - } + /** + * @return height of the plot area. Does not include margins. + */ + @Value.Default + public int height() { + return 900; + } - @Value.Default - public int leftMargin() { - return 120; - } + @Value.Default + public int leftMargin() { + return 120; + } - /** If true, write each legend entry's name in the color of its dataset */ - @Value.Default - public boolean legendColor() { - return false; - } + /** If true, write each legend entry's name in the color of its dataset */ + @Value.Default + public boolean legendColor() { + return false; + } - @Value.Default - public Font legendFont() { - return new Font("Helvetica", Font.BOLD, 12); - } + @Value.Default + public Font legendFont() { + return new Font("Helvetica", Font.BOLD, 12); + } - /** If true, draw a black outline around the characters in the name of each legend entry */ - @Value.Default - public boolean legendOutline() { - return false; - } + /** If true, draw a black outline around the characters in the name of each legend entry */ + @Value.Default + public boolean legendOutline() { + return false; + } - @Value.Default - public Point2D.Double legendPosition() { - double legendX = width() - 2 * rightMargin(); - double legendY = 1.5 * topMargin(); - return new Point2D.Double(legendX, legendY); - } + @Value.Default + public Point2D.Double legendPosition() { + double legendX = width() - 2 * rightMargin(); + double legendY = 1.5 * topMargin(); + return new Point2D.Double(legendX, legendY); + } - @Value.Default - public int rightMargin() { - return 80; - } + @Value.Default + public int rightMargin() { + return 80; + } - @Value.Default - public String title() { - return ""; - } + @Value.Default + public String title() { + return ""; + } - @Value.Default - public Font titleFont() { - return new Font("Times New Roman", Font.BOLD, 24); - } + @Value.Default + public Font titleFont() { + return new Font("Times New Roman", Font.BOLD, 24); + } - @Value.Default - public int topMargin() { - return 80; - } + @Value.Default + public int topMargin() { + return 80; + } - /** - * @return width of the plot area. Does not include margins. - */ - @Value.Default - public int width() { - return 1200; - } + /** + * @return width of the plot area. Does not include margins. + */ + @Value.Default + public int width() { + return 1200; + } - @Value.Default - public double xMajorTickLength() { - int minDimension = Math.min(width(), height()); - return 0.02 * minDimension; - } + @Value.Default + public double xMajorTickLength() { + int minDimension = Math.min(width(), height()); + return 0.02 * minDimension; + } - @Value.Default - public double xMinorTickLength() { - return 0.5 * xMajorTickLength(); - } + @Value.Default + public double xMinorTickLength() { + return 0.5 * xMajorTickLength(); + } - @Value.Default - public double yMajorTickLength() { - int minDimension = Math.min(width(), height()); - return 0.02 * minDimension; - } + @Value.Default + public double yMajorTickLength() { + int minDimension = Math.min(width(), height()); + return 0.02 * minDimension; + } - @Value.Default - public double yMinorTickLength() { - return 0.5 * yMajorTickLength(); - } + @Value.Default + public double yMinorTickLength() { + return 0.5 * yMajorTickLength(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/Activity.java b/src/main/java/terrasaur/utils/saaPlotLib/data/Activity.java index dd17a16..64353bc 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/Activity.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/Activity.java @@ -37,26 +37,30 @@ import terrasaur.utils.saaPlotLib.util.LegendEntry; @Value.Immutable public abstract class Activity { - public abstract String name(); + public abstract String name(); - public abstract Color color(); + public abstract Color color(); - public abstract Optional symbol(); + public abstract Optional symbol(); - private final IntervalSet.Builder intervals = IntervalSet.builder(); + private final IntervalSet.Builder intervals = IntervalSet.builder(); - public List getIntervals() { - IntervalSet intervalSet= intervals.build(); - List intervalList = new ArrayList<>(); - for (UnwritableInterval i : intervalSet) intervalList.add(new Interval(i.getBegin(), i.getEnd())); - return intervalList; - } + public List getIntervals() { + IntervalSet intervalSet = intervals.build(); + List intervalList = new ArrayList<>(); + for (UnwritableInterval i : intervalSet) intervalList.add(new Interval(i.getBegin(), i.getEnd())); + return intervalList; + } - public void addInterval(Interval interval) { - intervals.add(new UnwritableInterval(interval.getInf(), interval.getSup())); - } + public void addInterval(Interval interval) { + intervals.add(new UnwritableInterval(interval.getInf(), interval.getSup())); + } - public LegendEntry getLegendEntry() { - return ImmutableLegendEntry.builder().name(name()).color(color()).symbol(symbol()).build(); - } + public LegendEntry getLegendEntry() { + return ImmutableLegendEntry.builder() + .name(name()) + .color(color()) + .symbol(symbol()) + .build(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/ActivitySet.java b/src/main/java/terrasaur/utils/saaPlotLib/data/ActivitySet.java index 787ad3c..7b3be76 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/ActivitySet.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/ActivitySet.java @@ -32,52 +32,51 @@ import picante.timeline.StateTimeline; public class ActivitySet { - private final StateTimeline.Builder> builder; - private String setName; + private final StateTimeline.Builder> builder; + private String setName; - public ActivitySet(String setName) { - this.setName = setName; - builder = - StateTimeline.create( - new UnwritableInterval(-Double.MAX_VALUE, Double.MAX_VALUE), Optional.empty()); - } - - public String getName() { - return setName; - } - - public void setName(String setName) { - this.setName = setName; - } - - /** - * @param a activity to add. Overlapping activities will be overwritten. - */ - public void addActivity(Activity a) { - for (Interval interval : a.getIntervals()) { - if (interval.getSize() > 0) builder.add(new UnwritableInterval(interval.getInf(), interval.getSup()), Optional.of(a)); + public ActivitySet(String setName) { + this.setName = setName; + builder = StateTimeline.create(new UnwritableInterval(-Double.MAX_VALUE, Double.MAX_VALUE), Optional.empty()); } - } - /** - * @return a sorted list of intervals and activities. - */ - public Map getActivityMap() { - StateTimeline> timeline = getTimeline(); - - Map activityMap = new LinkedHashMap<>(); - for (Entry> entry : timeline.getEntries()) { - UnwritableInterval interval = entry.getKey(); - Optional activity = entry.getValue(); - activity.ifPresent(value -> activityMap.put(new Interval(interval.getBegin(), interval.getEnd()), value)); + public String getName() { + return setName; } - return activityMap; - } - /** - * @return {@link StateTimeline} of activities. - */ - private StateTimeline> getTimeline() { - return builder.build(); - } + public void setName(String setName) { + this.setName = setName; + } + + /** + * @param a activity to add. Overlapping activities will be overwritten. + */ + public void addActivity(Activity a) { + for (Interval interval : a.getIntervals()) { + if (interval.getSize() > 0) + builder.add(new UnwritableInterval(interval.getInf(), interval.getSup()), Optional.of(a)); + } + } + + /** + * @return a sorted list of intervals and activities. + */ + public Map getActivityMap() { + StateTimeline> timeline = getTimeline(); + + Map activityMap = new LinkedHashMap<>(); + for (Entry> entry : timeline.getEntries()) { + UnwritableInterval interval = entry.getKey(); + Optional activity = entry.getValue(); + activity.ifPresent(value -> activityMap.put(new Interval(interval.getBegin(), interval.getEnd()), value)); + } + return activityMap; + } + + /** + * @return {@link StateTimeline} of activities. + */ + private StateTimeline> getTimeline() { + return builder.build(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/Annotation.java b/src/main/java/terrasaur/utils/saaPlotLib/data/Annotation.java index 2f284d6..79aa5e3 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/Annotation.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/Annotation.java @@ -29,33 +29,32 @@ import terrasaur.utils.saaPlotLib.util.Keyword; /** * Class defining text annotations to display on plot - * + * * @author nairah1 * */ @Value.Immutable public abstract class Annotation { - public abstract String text(); + public abstract String text(); - @Value.Default - public Color color() { - return Color.lightGray; - } + @Value.Default + public Color color() { + return Color.lightGray; + } - @Value.Default - public Font font() { - return new Font("Helvetica", Font.PLAIN, 12); - } + @Value.Default + public Font font() { + return new Font("Helvetica", Font.PLAIN, 12); + } - @Value.Default - public Keyword verticalAlignment() { - return Keyword.ALIGN_CENTER; - } - - @Value.Default - public Keyword horizontalAlignment() { - return Keyword.ALIGN_CENTER; - } + @Value.Default + public Keyword verticalAlignment() { + return Keyword.ALIGN_CENTER; + } + @Value.Default + public Keyword horizontalAlignment() { + return Keyword.ALIGN_CENTER; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/Annotations.java b/src/main/java/terrasaur/utils/saaPlotLib/data/Annotations.java index 687fd78..16158b9 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/Annotations.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/Annotations.java @@ -33,44 +33,44 @@ import java.util.Map; */ public class Annotations extends PointList { - private final Map map; + private final Map map; - public Annotations() { - map = new HashMap<>(); - } + public Annotations() { + map = new HashMap<>(); + } - public void addAnnotation(Annotation annotation, double x, double y) { - map.put(size(), annotation); - add(x, y); - } + public void addAnnotation(Annotation annotation, double x, double y) { + map.put(size(), annotation); + add(x, y); + } - /** - * - * @param i index in the PointList - * @return the annotation on point at index i - */ - public Annotation getAnnotation(int i) { - Point4D p = get(i); - return getAnnotation(p); - } + /** + * + * @param i index in the PointList + * @return the annotation on point at index i + */ + public Annotation getAnnotation(int i) { + Point4D p = get(i); + return getAnnotation(p); + } - /** - * Return an annotation given a point p. Note that this point object MUST have been previously - * supplied to the Annotations class via {@link #addAnnotation(Annotation, double, double)}. - * - *

    Example: - * - *

    -   * for (Point4D p : annotations) {
    -   *   Annotation a = annotations.getAnnotation(p);
    -   *   // do something
    -   * }
    -   * 
    - * - * @param p point - * @return annotation, or null if not defined - */ - public Annotation getAnnotation(Point4D p) { - return map.get(p.getIndex()); - } + /** + * Return an annotation given a point p. Note that this point object MUST have been previously + * supplied to the Annotations class via {@link #addAnnotation(Annotation, double, double)}. + * + *

    Example: + * + *

    +     * for (Point4D p : annotations) {
    +     *   Annotation a = annotations.getAnnotation(p);
    +     *   // do something
    +     * }
    +     * 
    + * + * @param p point + * @return annotation, or null if not defined + */ + public Annotation getAnnotation(Point4D p) { + return map.get(p.getIndex()); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/DiscreteDataSet.java b/src/main/java/terrasaur/utils/saaPlotLib/data/DiscreteDataSet.java index 63c4ffd..4ea3d51 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/DiscreteDataSet.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/DiscreteDataSet.java @@ -37,195 +37,196 @@ import terrasaur.utils.saaPlotLib.util.LegendEntry; public class DiscreteDataSet { - protected Color color; - protected ColorRamp colorRamp; - protected PointList data; - protected Stroke stroke; - protected DescriptiveStatistics xStats; - protected DescriptiveStatistics yStats; - protected String name; - protected PLOTTYPE plotType; - protected Symbol symbol; + protected Color color; + protected ColorRamp colorRamp; + protected PointList data; + protected Stroke stroke; + protected DescriptiveStatistics xStats; + protected DescriptiveStatistics yStats; + protected String name; + protected PLOTTYPE plotType; + protected Symbol symbol; - public DiscreteDataSet(String name) { - this.name = name; - color = Color.blue; - colorRamp = null; - data = new PointList(); - stroke = new BasicStroke(1.5f); - plotType = PLOTTYPE.LINE; - symbol = null; - } - - public void setTo(DiscreteDataSet other) { - this.color = other.color; - this.colorRamp = other.colorRamp; - this.data = other.data; - this.stroke = other.stroke; - this.xStats = other.xStats; - this.yStats = other.yStats; - this.name = other.name; - } - - /** - * @param x X value - * @param y Y value - */ - public void add(double x, double y) { - data.add(x, y); - } - - /** - * @param x X value - * @param y Y value - * @param z Z value - */ - public void add(double x, double y, double z) { - data.add(x, y, z); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. - * - * @param x X value - * @param y Y value - * @param z Z value - * @param w W value (this is often a property of the 3D point) - */ - public void add(double x, double y, double z, double w) { - data.add(x, y, z, w); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. - * - * @param xyz 3D coordinates - * @param w W value (this is often a property of the 3D point) - */ - public void add(Point3D xyz, double w) { - data.add(xyz.x(), xyz.y(), xyz.z(), w); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() - * - x.getY(), x.getX() + x.getZ()) and similarly for y and z. - * - * @param x x value with error bars - * @param y x value with error bars - * @param z x value with error bars - * @param w W value (this is often a property of the 3D point) - */ - public void add(Point3D x, Point3D y, Point3D z, double w) { - data.add(x, y, z, w); - } - - public void add(List x, List y) { - for (int i = 0; i < x.size(); i++) data.add(x.get(i), y.get(i)); - } - - public void add(Map map) { - for (double key : map.keySet()) { - data.add(key, map.get(key)); + public DiscreteDataSet(String name) { + this.name = name; + color = Color.blue; + colorRamp = null; + data = new PointList(); + stroke = new BasicStroke(1.5f); + plotType = PLOTTYPE.LINE; + symbol = null; } - } - public LegendEntry getLegendEntry() { - ImmutableLegendEntry.Builder builder = ImmutableLegendEntry.builder().name(name).color(color); - if (stroke != null) builder.stroke(stroke); - if (symbol != null) builder.symbol(symbol); - return builder.build(); - } + public void setTo(DiscreteDataSet other) { + this.color = other.color; + this.colorRamp = other.colorRamp; + this.data = other.data; + this.stroke = other.stroke; + this.xStats = other.xStats; + this.yStats = other.yStats; + this.name = other.name; + } - public Color getColor() { - return color; - } + /** + * @param x X value + * @param y Y value + */ + public void add(double x, double y) { + data.add(x, y); + } - /** - * @param color color for this data set - */ - public void setColor(Color color) { - this.color = color; - } + /** + * @param x X value + * @param y Y value + * @param z Z value + */ + public void add(double x, double y, double z) { + data.add(x, y, z); + } - public ColorRamp getColorRamp() { - return colorRamp; - } + /** + * Add a new point with coordinates (x, y, z) and property w. + * + * @param x X value + * @param y Y value + * @param z Z value + * @param w W value (this is often a property of the 3D point) + */ + public void add(double x, double y, double z, double w) { + data.add(x, y, z, w); + } - /** - * @param colorRamp color ramp. Presently only implemented for plotting symbols. - */ - public void setColorRamp(ColorRamp colorRamp) { - this.colorRamp = colorRamp; - } + /** + * Add a new point with coordinates (x, y, z) and property w. + * + * @param xyz 3D coordinates + * @param w W value (this is often a property of the 3D point) + */ + public void add(Point3D xyz, double w) { + data.add(xyz.x(), xyz.y(), xyz.z(), w); + } - public PointList getData() { - return data; - } + /** + * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() + * - x.getY(), x.getX() + x.getZ()) and similarly for y and z. + * + * @param x x value with error bars + * @param y x value with error bars + * @param z x value with error bars + * @param w W value (this is often a property of the 3D point) + */ + public void add(Point3D x, Point3D y, Point3D z, double w) { + data.add(x, y, z, w); + } - /** clear the data array */ - public void clear() { - data = new PointList(); - } + public void add(List x, List y) { + for (int i = 0; i < x.size(); i++) data.add(x.get(i), y.get(i)); + } - public AxisX defaultXAxis(String title) { - DescriptiveStatistics xStats = getXStats(); - return new AxisX(xStats.getMin(), xStats.getMax(), title); - } + public void add(Map map) { + for (double key : map.keySet()) { + data.add(key, map.get(key)); + } + } - public AxisY defaultYAxis(String title) { - DescriptiveStatistics yStats = getYStats(); - return new AxisY(yStats.getMin(), yStats.getMax(), title); - } + public LegendEntry getLegendEntry() { + ImmutableLegendEntry.Builder builder = + ImmutableLegendEntry.builder().name(name).color(color); + if (stroke != null) builder.stroke(stroke); + if (symbol != null) builder.symbol(symbol); + return builder.build(); + } - public DescriptiveStatistics getXStats() { + public Color getColor() { + return color; + } - DescriptiveStatistics stats = new DescriptiveStatistics(); - for (double x : data.getX()) stats.addValue(x); + /** + * @param color color for this data set + */ + public void setColor(Color color) { + this.color = color; + } - return stats; - } + public ColorRamp getColorRamp() { + return colorRamp; + } - public DescriptiveStatistics getYStats() { + /** + * @param colorRamp color ramp. Presently only implemented for plotting symbols. + */ + public void setColorRamp(ColorRamp colorRamp) { + this.colorRamp = colorRamp; + } - DescriptiveStatistics stats = new DescriptiveStatistics(); - for (double y : data.getY()) stats.addValue(y); + public PointList getData() { + return data; + } - return stats; - } + /** clear the data array */ + public void clear() { + data = new PointList(); + } - public Stroke getStroke() { - return stroke; - } + public AxisX defaultXAxis(String title) { + DescriptiveStatistics xStats = getXStats(); + return new AxisX(xStats.getMin(), xStats.getMax(), title); + } - public void setStroke(Stroke stroke) { - this.stroke = stroke; - } + public AxisY defaultYAxis(String title) { + DescriptiveStatistics yStats = getYStats(); + return new AxisY(yStats.getMin(), yStats.getMax(), title); + } - public String getName() { - return name; - } + public DescriptiveStatistics getXStats() { - public PLOTTYPE getPlotType() { - return plotType; - } + DescriptiveStatistics stats = new DescriptiveStatistics(); + for (double x : data.getX()) stats.addValue(x); - public void setPlotType(PLOTTYPE plotType) { - this.plotType = plotType; - } + return stats; + } - public Symbol getSymbol() { - return symbol; - } + public DescriptiveStatistics getYStats() { - public void setSymbol(Symbol symbol) { - this.symbol = symbol; - setPlotType(PLOTTYPE.SYMBOL); - } + DescriptiveStatistics stats = new DescriptiveStatistics(); + for (double y : data.getY()) stats.addValue(y); - public enum PLOTTYPE { - BAR, - LINE, - SAND, - SYMBOL - } + return stats; + } + + public Stroke getStroke() { + return stroke; + } + + public void setStroke(Stroke stroke) { + this.stroke = stroke; + } + + public String getName() { + return name; + } + + public PLOTTYPE getPlotType() { + return plotType; + } + + public void setPlotType(PLOTTYPE plotType) { + this.plotType = plotType; + } + + public Symbol getSymbol() { + return symbol; + } + + public void setSymbol(Symbol symbol) { + this.symbol = symbol; + setPlotType(PLOTTYPE.SYMBOL); + } + + public enum PLOTTYPE { + BAR, + LINE, + SAND, + SYMBOL + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/HistogramDataSet.java b/src/main/java/terrasaur/utils/saaPlotLib/data/HistogramDataSet.java index 3fd3509..bb8f619 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/HistogramDataSet.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/HistogramDataSet.java @@ -29,76 +29,76 @@ import java.util.TreeSet; public class HistogramDataSet extends DiscreteDataSet { - private final List binBoundaryList; - private final NavigableSet binBoundarySet; - private List yValues; + private final List binBoundaryList; + private final NavigableSet binBoundarySet; + private List yValues; - public int getTotal() { - return yValues.size(); - } - - public List getYValues() { - return new ArrayList<>(yValues); - } - - public HistogramDataSet(String name, List binBoundaryList) { - super(name); - this.binBoundaryList = binBoundaryList; - binBoundarySet = new TreeSet<>(binBoundaryList); - - // initialize all bins to 0 - for (int i = 1; i < binBoundaryList.size(); i++) { - double binCenter = (binBoundaryList.get(i - 1) + binBoundaryList.get(i)) / 2; - data.add(binCenter, 0.); + public int getTotal() { + return yValues.size(); } - yValues = new ArrayList<>(); - } - - public void add(double y) { - Double lowerBound = binBoundarySet.floor(y); - Double upperBound = binBoundarySet.higher(y); - if (lowerBound == null) lowerBound = binBoundarySet.first(); - if (upperBound == null) upperBound = binBoundarySet.last(); - - double binCenter = (lowerBound + upperBound) / 2; - - int index = data.getX().indexOf(binCenter); - if (index != -1) { - // this value is contained within one of the bins - Point4D p = data.get(index); - data.set(p.getIndex(), p.getX(), p.getY() + 1); + public List getYValues() { + return new ArrayList<>(yValues); } - yValues.add(y); - } + public HistogramDataSet(String name, List binBoundaryList) { + super(name); + this.binBoundaryList = binBoundaryList; + binBoundarySet = new TreeSet<>(binBoundaryList); - @Override - public void add(double x, double y) { - add(y); - } + // initialize all bins to 0 + for (int i = 1; i < binBoundaryList.size(); i++) { + double binCenter = (binBoundaryList.get(i - 1) + binBoundaryList.get(i)) / 2; + data.add(binCenter, 0.); + } - /** - * @param name name of new dataset - * @return dataset where the values all sum to 1 - */ - public HistogramDataSet getNormalized(String name) { - HistogramDataSet ds = new HistogramDataSet(name, binBoundaryList); - PointList normalized = new PointList(); - for (Point4D p : data) { - normalized.add(p.getX(), p.getY() / getTotal()); + yValues = new ArrayList<>(); } - ds.data = normalized; - ds.yValues = new ArrayList<>(yValues); - return ds; - } + public void add(double y) { + Double lowerBound = binBoundarySet.floor(y); + Double upperBound = binBoundarySet.higher(y); + if (lowerBound == null) lowerBound = binBoundarySet.first(); + if (upperBound == null) upperBound = binBoundarySet.last(); - public List getBinBoundaryList() { - return binBoundaryList; - } + double binCenter = (lowerBound + upperBound) / 2; - public NavigableSet getBinBoundarySet() { - return binBoundarySet; - } + int index = data.getX().indexOf(binCenter); + if (index != -1) { + // this value is contained within one of the bins + Point4D p = data.get(index); + data.set(p.getIndex(), p.getX(), p.getY() + 1); + } + + yValues.add(y); + } + + @Override + public void add(double x, double y) { + add(y); + } + + /** + * @param name name of new dataset + * @return dataset where the values all sum to 1 + */ + public HistogramDataSet getNormalized(String name) { + HistogramDataSet ds = new HistogramDataSet(name, binBoundaryList); + PointList normalized = new PointList(); + for (Point4D p : data) { + normalized.add(p.getX(), p.getY() / getTotal()); + } + ds.data = normalized; + ds.yValues = new ArrayList<>(yValues); + + return ds; + } + + public List getBinBoundaryList() { + return binBoundaryList; + } + + public NavigableSet getBinBoundarySet() { + return binBoundarySet; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/Point3D.java b/src/main/java/terrasaur/utils/saaPlotLib/data/Point3D.java index a42be49..63fd7e4 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/Point3D.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/Point3D.java @@ -22,5 +22,4 @@ */ package terrasaur.utils.saaPlotLib.data; -public record Point3D(double x,double y,double z) { -} +public record Point3D(double x, double y, double z) {} diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/Point4D.java b/src/main/java/terrasaur/utils/saaPlotLib/data/Point4D.java index 18549b2..e1de9a6 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/Point4D.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/Point4D.java @@ -27,147 +27,142 @@ import java.util.Comparator; public class Point4D { - public static final Comparator SORT_ON_I = - Comparator.comparingInt(Point4D::getIndex); + public static final Comparator SORT_ON_I = Comparator.comparingInt(Point4D::getIndex); - public static final Comparator SORT_ON_X = - Comparator.comparingDouble(Point4D::getX); + public static final Comparator SORT_ON_X = Comparator.comparingDouble(Point4D::getX); - public static final Comparator SORT_ON_Y = - Comparator.comparingDouble(Point4D::getY); + public static final Comparator SORT_ON_Y = Comparator.comparingDouble(Point4D::getY); - public static final Comparator SORT_ON_Z = - Comparator.comparingDouble(Point4D::getZ); + public static final Comparator SORT_ON_Z = Comparator.comparingDouble(Point4D::getZ); - public static final Comparator SORT_ON_W = - Comparator.comparingDouble(Point4D::getW); - protected final double x; - protected final double y; - protected final double z; // 3d coordinate - protected final Point2D xError; - protected final Point2D yError; - protected final Point2D zError; - protected final double w; // property - protected int index; + public static final Comparator SORT_ON_W = Comparator.comparingDouble(Point4D::getW); + protected final double x; + protected final double y; + protected final double z; // 3d coordinate + protected final Point2D xError; + protected final Point2D yError; + protected final Point2D zError; + protected final double w; // property + protected int index; - public Point4D(int i, double x, double y) { - this(i, x, y, Double.NaN, Double.NaN); - } + public Point4D(int i, double x, double y) { + this(i, x, y, Double.NaN, Double.NaN); + } - public Point4D(int i, Point2D x, Point2D y) { - this(i, x, y, new Point2D.Double(Double.NaN, Double.NaN), Double.NaN); - } + public Point4D(int i, Point2D x, Point2D y) { + this(i, x, y, new Point2D.Double(Double.NaN, Double.NaN), Double.NaN); + } - public Point4D(int i, Point3D x, Point3D y) { - this(i, x, y, new Point3D(Double.NaN, Double.NaN, Double.NaN), Double.NaN); - } + public Point4D(int i, Point3D x, Point3D y) { + this(i, x, y, new Point3D(Double.NaN, Double.NaN, Double.NaN), Double.NaN); + } - public Point4D(int i, double x, double y, double z) { - this(i, x, y, z, Double.NaN); - } + public Point4D(int i, double x, double y, double z) { + this(i, x, y, z, Double.NaN); + } - public Point4D(int i, Point2D x, Point2D y, Point2D z) { - this(i, x, y, z, Double.NaN); - } + public Point4D(int i, Point2D x, Point2D y, Point2D z) { + this(i, x, y, z, Double.NaN); + } - public Point4D(int i, Point3D x, Point3D y, Point3D z) { - this(i, x, y, z, Double.NaN); - } + public Point4D(int i, Point3D x, Point3D y, Point3D z) { + this(i, x, y, z, Double.NaN); + } - public Point4D(int i, double x, double y, double z, double w) { - this( - i, - new Point3D(x, Double.NaN, Double.NaN), - new Point3D(y, Double.NaN, Double.NaN), - new Point3D(z, Double.NaN, Double.NaN), - w); - } + public Point4D(int i, double x, double y, double z, double w) { + this( + i, + new Point3D(x, Double.NaN, Double.NaN), + new Point3D(y, Double.NaN, Double.NaN), + new Point3D(z, Double.NaN, Double.NaN), + w); + } - /** - * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getY()) and similarly for y and z. - * - * @param i index - * @param x x - * @param y y - * @param z z - * @param w property value - */ - public Point4D(int i, Point2D x, Point2D y, Point2D z, double w) { - this( - i, - new Point3D(x.getX(), x.getY(), x.getY()), - new Point3D(y.getX(), y.getY(), y.getY()), - new Point3D(z.getX(), z.getY(), z.getY()), - w); - } + /** + * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getY()) and similarly for y and z. + * + * @param i index + * @param x x + * @param y y + * @param z z + * @param w property value + */ + public Point4D(int i, Point2D x, Point2D y, Point2D z, double w) { + this( + i, + new Point3D(x.getX(), x.getY(), x.getY()), + new Point3D(y.getX(), y.getY(), y.getY()), + new Point3D(z.getX(), z.getY(), z.getY()), + w); + } - /** - * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getZ()) and similarly for y and z. - * - * @param index index - * @param x x - * @param y y - * @param z z - * @param w property value - */ - public Point4D(int index, Point3D x, Point3D y, Point3D z, double w) { - this.index = index; - this.x = x.x(); - this.y = y.x(); - this.z = z.x(); - this.xError = new Point2D.Double(x.y(), x.z()); - this.yError = new Point2D.Double(y.y(), y.z()); - this.zError = new Point2D.Double(z.y(), z.z()); - this.w = w; - } + /** + * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getZ()) and similarly for y and z. + * + * @param index index + * @param x x + * @param y y + * @param z z + * @param w property value + */ + public Point4D(int index, Point3D x, Point3D y, Point3D z, double w) { + this.index = index; + this.x = x.x(); + this.y = y.x(); + this.z = z.x(); + this.xError = new Point2D.Double(x.y(), x.z()); + this.yError = new Point2D.Double(y.y(), y.z()); + this.zError = new Point2D.Double(z.y(), z.z()); + this.w = w; + } - /** - * Create a deep copy of the supplied point. - * - * @param other point to copy - */ - public Point4D(Point4D other) { - this.index = other.index; - this.x = other.x; - this.y = other.y; - this.z = other.z; - this.w = other.w; - this.xError = new Point2D.Double(other.xError.getX(), other.xError.getY()); - this.yError = new Point2D.Double(other.yError.getX(), other.yError.getY()); - this.zError = new Point2D.Double(other.zError.getX(), other.zError.getY()); - } + /** + * Create a deep copy of the supplied point. + * + * @param other point to copy + */ + public Point4D(Point4D other) { + this.index = other.index; + this.x = other.x; + this.y = other.y; + this.z = other.z; + this.w = other.w; + this.xError = new Point2D.Double(other.xError.getX(), other.xError.getY()); + this.yError = new Point2D.Double(other.yError.getX(), other.yError.getY()); + this.zError = new Point2D.Double(other.zError.getX(), other.zError.getY()); + } - public int getIndex() { - return index; - } + public int getIndex() { + return index; + } - public double getX() { - return x; - } + public double getX() { + return x; + } - public double getY() { - return y; - } + public double getY() { + return y; + } - public double getZ() { - return z; - } + public double getZ() { + return z; + } - public Point2D getXError() { - return xError; - } + public Point2D getXError() { + return xError; + } - public Point2D getYError() { - return yError; - } + public Point2D getYError() { + return yError; + } - public Point2D getZError() { - return zError; - } + public Point2D getZError() { + return zError; + } - public double getW() { - return w; - } + public double getW() { + return w; + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/data/PointList.java b/src/main/java/terrasaur/utils/saaPlotLib/data/PointList.java index be15587..9a697df 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/data/PointList.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/data/PointList.java @@ -32,332 +32,333 @@ import terrasaur.utils.saaPlotLib.colorMaps.ColorRamp; public class PointList implements Iterable { - protected COORDINATE sortedOn; - protected final List pointList; - private ColorRamp colorRamp; + protected COORDINATE sortedOn; + protected final List pointList; + private ColorRamp colorRamp; - public PointList() { - sortedOn = COORDINATE.I; - pointList = new ArrayList<>(); - colorRamp = null; - } - - public ColorRamp getColorRamp() { - return colorRamp; - } - - public void setColorRamp(ColorRamp colorRamp) { - this.colorRamp = colorRamp; - } - - public void add(Point4D p) { - p.index = pointList.size(); - pointList.add(p); - } - - /** - * Add a new point with coordinates (x, y). - * - * @param x x coordinate - * @param y y coordinate - */ - public void add(double x, double y) { - pointList.add(new Point4D(pointList.size(), x, y)); - } - - /** - * Replace point at index i with coordinates (x, y). - * - * @param x x coordinate - * @param y y coordinate - */ - public void set(int i, double x, double y) { - pointList.set(i, new Point4D(i, x, y)); - } - - /** - * Add a new point with coordinates (x, y). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getY()) and similarly for y. - * - * @param x x coordinate - * @param y y coordinate - */ - public void add(Point2D x, Point2D y) { - pointList.add(new Point4D(pointList.size(), x, y)); - } - - /** - * Add a new point with coordinates (x, y). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getZ()) and similarly for y. - * - * @param x x coordinate - * @param y y coordinate - */ - public void add(Point3D x, Point3D y) { - pointList.add(new Point4D(pointList.size(), x, y)); - } - - /** - * Add a new point with coordinates (x, y, z). - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - */ - public void add(double x, double y, double z) { - pointList.add(new Point4D(pointList.size(), x, y, z)); - } - - /** - * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getY()) and similarly for y and z. - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - */ - public void add(Point2D x, Point2D y, Point2D z) { - pointList.add(new Point4D(pointList.size(), x, y, z)); - } - - /** - * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), - * x.getX() + x.getZ()) and similarly for y and z. - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - */ - public void add(Point3D x, Point3D y, Point3D z) { - pointList.add(new Point4D(pointList.size(), x, y, z)); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - * @param w property value - */ - public void add(double x, double y, double z, double w) { - pointList.add(new Point4D(pointList.size(), x, y, z, w)); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() - * - x.getY(), x.getX() + x.getY()) and similarly for y and z. - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - * @param w property value - */ - public void add(Point2D x, Point2D y, Point2D z, double w) { - pointList.add(new Point4D(pointList.size(), x, y, z, w)); - } - - /** - * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() - * - x.getY(), x.getX() + x.getZ()) and similarly for y and z. - * - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - * @param w property value - */ - public void add(Point3D x, Point3D y, Point3D z, double w) { - pointList.add(new Point4D(pointList.size(), x, y, z, w)); - } - - /** - * Sort the dataset on the desired {@link COORDINATE}. - * - * @param c coordinate to use for sorting - */ - public void sort(COORDINATE c) { - switch (c) { - case I: - sortOnIndex(); - break; - case W: - sortOnW(); - break; - case X: - sortOnX(); - break; - case Y: - sortOnY(); - break; - case Z: - sortOnZ(); - break; + public PointList() { + sortedOn = COORDINATE.I; + pointList = new ArrayList<>(); + colorRamp = null; } - } - /** Sort the data points by insertion order. */ - private void sortOnIndex() { - if (sortedOn != COORDINATE.I) { - pointList.sort(Point4D.SORT_ON_I); - sortedOn = COORDINATE.I; + public ColorRamp getColorRamp() { + return colorRamp; } - } - /** Sort the data points by X value. */ - private void sortOnX() { - if (sortedOn != COORDINATE.X) { - pointList.sort(Point4D.SORT_ON_X); - sortedOn = COORDINATE.X; + public void setColorRamp(ColorRamp colorRamp) { + this.colorRamp = colorRamp; } - } - /** Sort the data points by Y value. */ - private void sortOnY() { - if (sortedOn != COORDINATE.Y) { - pointList.sort(Point4D.SORT_ON_Y); - sortedOn = COORDINATE.Y; + public void add(Point4D p) { + p.index = pointList.size(); + pointList.add(p); } - } - /** Sort the data points by Z value. */ - private void sortOnZ() { - if (sortedOn != COORDINATE.Z) { - pointList.sort(Point4D.SORT_ON_Z); - sortedOn = COORDINATE.Z; + /** + * Add a new point with coordinates (x, y). + * + * @param x x coordinate + * @param y y coordinate + */ + public void add(double x, double y) { + pointList.add(new Point4D(pointList.size(), x, y)); } - } - /** Sort the data points by W value. */ - private void sortOnW() { - if (sortedOn != COORDINATE.W) { - pointList.sort(Point4D.SORT_ON_W); - sortedOn = COORDINATE.W; + /** + * Replace point at index i with coordinates (x, y). + * + * @param x x coordinate + * @param y y coordinate + */ + public void set(int i, double x, double y) { + pointList.set(i, new Point4D(i, x, y)); } - } - /** - * Returns the element at the specified position in this list. - * - * @see List#get(int) - * @param index index of the element to return - * @return the element at the specified position in this list - */ - public Point4D get(int index) { - return pointList.get(index); - } - - /** - * - * @return the first element in the list. Be sure to sort the list on the desired coordinate before - * calling this function. - */ - public Point4D getFirst() { - return pointList.get(0); - } - - /** - * - * @return the last element in the list. Be sure to sort the list on the desired coordinate before - * calling this function. - */ - public Point4D getLast() { - return pointList.get(pointList.size() - 1); - } - - /** - * - * @return a list of the X coordinates, which may not be sorted - */ - public List getX() { - return pointList.stream().map(Point4D::getX).collect(Collectors.toList()); - } - - /** - * - * @return a list of the Y coordinates, which may not be sorted - */ - public List getY() { - return pointList.stream().map(Point4D::getY).collect(Collectors.toList()); - } - - /** - * - * @param x x value - * @return the linearly interpolated y value at x - */ - public double getY(double x) { - PointList pl = subSetX(x, x); - - if (pl.size() == 1) return pl.getFirst().getY(); - - double frac = (x - pl.getFirst().getX()) / (pl.getLast().getX() - pl.getFirst().getX()); - return pl.getFirst().getY() + frac * (pl.getLast().getY() - pl.getFirst().getY()); - } - - /** - * - * @return the number of elements in this list. If this list contains more than Integer.MAX_VALUE - * elements, returns Integer.MAX_VALUE. - */ - public int size() { - return pointList.size(); - } - - /** - * - * @param xMin minimum X value - * @param xMax maximum X value - * @return PointList of all points with X coordinates bounding xMin and xMax. Points are sorted on - * X. - */ - public PointList subSetX(double xMin, double xMax) { - PointList sortedOnX = new PointList(); - for (Point4D p : this) sortedOnX.add(p); - sortedOnX.sort(COORDINATE.X); - - List x = sortedOnX.getX(); - - int minIndex = Collections.binarySearch(x, xMin); - if (minIndex < 0) minIndex = Math.abs(minIndex) - 1; - int maxIndex = Collections.binarySearch(x, xMax); - if (maxIndex < 0) maxIndex = Math.abs(maxIndex) - 1; - - minIndex = Math.max(0, minIndex - 1); - maxIndex = Math.min(sortedOnX.size(), maxIndex + 1); - PointList pl = new PointList(); - for (int i = minIndex; i < maxIndex; i++) { - pl.add(sortedOnX.get(i)); + /** + * Add a new point with coordinates (x, y). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getY()) and similarly for y. + * + * @param x x coordinate + * @param y y coordinate + */ + public void add(Point2D x, Point2D y) { + pointList.add(new Point4D(pointList.size(), x, y)); } - return pl; - } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - for (Point4D p : this) { - sb.append( - String.format( - "%d %f %f %f %f %s\n", - p.getIndex(), - p.getX(), - p.getY(), - p.getZ(), - p.getW(), - colorRamp == null || Double.isNaN(p.getW()) ? "" : colorRamp.getColor(p.getW()))); + /** + * Add a new point with coordinates (x, y). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getZ()) and similarly for y. + * + * @param x x coordinate + * @param y y coordinate + */ + public void add(Point3D x, Point3D y) { + pointList.add(new Point4D(pointList.size(), x, y)); } - return sb.toString(); - } - @Override - public Iterator iterator() { - return pointList.iterator(); - } + /** + * Add a new point with coordinates (x, y, z). + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + */ + public void add(double x, double y, double z) { + pointList.add(new Point4D(pointList.size(), x, y, z)); + } - public enum COORDINATE { - X, - Y, - Z, - W, - I - } + /** + * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getY()) and similarly for y and z. + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + */ + public void add(Point2D x, Point2D y, Point2D z) { + pointList.add(new Point4D(pointList.size(), x, y, z)); + } + + /** + * Add a new point with coordinates (x, y, z). The error bounds on x are (x.getX() - x.getY(), + * x.getX() + x.getZ()) and similarly for y and z. + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + */ + public void add(Point3D x, Point3D y, Point3D z) { + pointList.add(new Point4D(pointList.size(), x, y, z)); + } + + /** + * Add a new point with coordinates (x, y, z) and property w. + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + * @param w property value + */ + public void add(double x, double y, double z, double w) { + pointList.add(new Point4D(pointList.size(), x, y, z, w)); + } + + /** + * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() + * - x.getY(), x.getX() + x.getY()) and similarly for y and z. + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + * @param w property value + */ + public void add(Point2D x, Point2D y, Point2D z, double w) { + pointList.add(new Point4D(pointList.size(), x, y, z, w)); + } + + /** + * Add a new point with coordinates (x, y, z) and property w. The error bounds on x are (x.getX() + * - x.getY(), x.getX() + x.getZ()) and similarly for y and z. + * + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + * @param w property value + */ + public void add(Point3D x, Point3D y, Point3D z, double w) { + pointList.add(new Point4D(pointList.size(), x, y, z, w)); + } + + /** + * Sort the dataset on the desired {@link COORDINATE}. + * + * @param c coordinate to use for sorting + */ + public void sort(COORDINATE c) { + switch (c) { + case I: + sortOnIndex(); + break; + case W: + sortOnW(); + break; + case X: + sortOnX(); + break; + case Y: + sortOnY(); + break; + case Z: + sortOnZ(); + break; + } + } + + /** Sort the data points by insertion order. */ + private void sortOnIndex() { + if (sortedOn != COORDINATE.I) { + pointList.sort(Point4D.SORT_ON_I); + sortedOn = COORDINATE.I; + } + } + + /** Sort the data points by X value. */ + private void sortOnX() { + if (sortedOn != COORDINATE.X) { + pointList.sort(Point4D.SORT_ON_X); + sortedOn = COORDINATE.X; + } + } + + /** Sort the data points by Y value. */ + private void sortOnY() { + if (sortedOn != COORDINATE.Y) { + pointList.sort(Point4D.SORT_ON_Y); + sortedOn = COORDINATE.Y; + } + } + + /** Sort the data points by Z value. */ + private void sortOnZ() { + if (sortedOn != COORDINATE.Z) { + pointList.sort(Point4D.SORT_ON_Z); + sortedOn = COORDINATE.Z; + } + } + + /** Sort the data points by W value. */ + private void sortOnW() { + if (sortedOn != COORDINATE.W) { + pointList.sort(Point4D.SORT_ON_W); + sortedOn = COORDINATE.W; + } + } + + /** + * Returns the element at the specified position in this list. + * + * @see List#get(int) + * @param index index of the element to return + * @return the element at the specified position in this list + */ + public Point4D get(int index) { + return pointList.get(index); + } + + /** + * + * @return the first element in the list. Be sure to sort the list on the desired coordinate before + * calling this function. + */ + public Point4D getFirst() { + return pointList.get(0); + } + + /** + * + * @return the last element in the list. Be sure to sort the list on the desired coordinate before + * calling this function. + */ + public Point4D getLast() { + return pointList.get(pointList.size() - 1); + } + + /** + * + * @return a list of the X coordinates, which may not be sorted + */ + public List getX() { + return pointList.stream().map(Point4D::getX).collect(Collectors.toList()); + } + + /** + * + * @return a list of the Y coordinates, which may not be sorted + */ + public List getY() { + return pointList.stream().map(Point4D::getY).collect(Collectors.toList()); + } + + /** + * + * @param x x value + * @return the linearly interpolated y value at x + */ + public double getY(double x) { + PointList pl = subSetX(x, x); + + if (pl.size() == 1) return pl.getFirst().getY(); + + double frac = (x - pl.getFirst().getX()) + / (pl.getLast().getX() - pl.getFirst().getX()); + return pl.getFirst().getY() + + frac * (pl.getLast().getY() - pl.getFirst().getY()); + } + + /** + * + * @return the number of elements in this list. If this list contains more than Integer.MAX_VALUE + * elements, returns Integer.MAX_VALUE. + */ + public int size() { + return pointList.size(); + } + + /** + * + * @param xMin minimum X value + * @param xMax maximum X value + * @return PointList of all points with X coordinates bounding xMin and xMax. Points are sorted on + * X. + */ + public PointList subSetX(double xMin, double xMax) { + PointList sortedOnX = new PointList(); + for (Point4D p : this) sortedOnX.add(p); + sortedOnX.sort(COORDINATE.X); + + List x = sortedOnX.getX(); + + int minIndex = Collections.binarySearch(x, xMin); + if (minIndex < 0) minIndex = Math.abs(minIndex) - 1; + int maxIndex = Collections.binarySearch(x, xMax); + if (maxIndex < 0) maxIndex = Math.abs(maxIndex) - 1; + + minIndex = Math.max(0, minIndex - 1); + maxIndex = Math.min(sortedOnX.size(), maxIndex + 1); + PointList pl = new PointList(); + for (int i = minIndex; i < maxIndex; i++) { + pl.add(sortedOnX.get(i)); + } + return pl; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Point4D p : this) { + sb.append(String.format( + "%d %f %f %f %f %s\n", + p.getIndex(), + p.getX(), + p.getY(), + p.getZ(), + p.getW(), + colorRamp == null || Double.isNaN(p.getW()) ? "" : colorRamp.getColor(p.getW()))); + } + return sb.toString(); + } + + @Override + public Iterator iterator() { + return pointList.iterator(); + } + + public enum COORDINATE { + X, + Y, + Z, + W, + I + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/ActivityPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/ActivityPlotDemo.java index 0c36ee4..a633cc2 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/ActivityPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/ActivityPlotDemo.java @@ -47,107 +47,114 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class ActivityPlotDemo { - public static BufferedImage makePlot() { + public static BufferedImage makePlot() { - List activities = new ArrayList<>(); - ActivitySet as = new ActivitySet("First"); + List activities = new ArrayList<>(); + ActivitySet as = new ActivitySet("First"); - Activity red = ImmutableActivity.builder().name(as.getName() + " Red").color(Color.RED).build(); - red.addInterval(new Interval(0.05, 0.35)); - red.addInterval(new Interval(0.45, 0.55)); - red.addInterval(new Interval(0.75, 0.85)); - as.addActivity(red); + Activity red = ImmutableActivity.builder() + .name(as.getName() + " Red") + .color(Color.RED) + .build(); + red.addInterval(new Interval(0.05, 0.35)); + red.addInterval(new Interval(0.45, 0.55)); + red.addInterval(new Interval(0.75, 0.85)); + as.addActivity(red); - Activity orange = - ImmutableActivity.builder().name(as.getName() + " Orange").color(Color.ORANGE).build(); - orange.addInterval(new Interval(0.30, 0.40)); - orange.addInterval(new Interval(0.60, 0.70)); - as.addActivity(orange); - activities.add(as); + Activity orange = ImmutableActivity.builder() + .name(as.getName() + " Orange") + .color(Color.ORANGE) + .build(); + orange.addInterval(new Interval(0.30, 0.40)); + orange.addInterval(new Interval(0.60, 0.70)); + as.addActivity(orange); + activities.add(as); - as = new ActivitySet("Second"); + as = new ActivitySet("Second"); - // this is a different activity than orange above, hence new object - orange = ImmutableActivity.builder().name(as.getName() + " Orange").color(Color.ORANGE).build(); - orange.addInterval(new Interval(0.50, 0.65)); - as.addActivity(orange); + // this is a different activity than orange above, hence new object + orange = ImmutableActivity.builder() + .name(as.getName() + " Orange") + .color(Color.ORANGE) + .build(); + orange.addInterval(new Interval(0.50, 0.65)); + as.addActivity(orange); - Activity blue = - ImmutableActivity.builder().name(as.getName() + " Blue").color(Color.BLUE).build(); - blue.addInterval(new Interval(0.35, 0.44)); - blue.addInterval(new Interval(0.6, 0.95)); - as.addActivity(blue); + Activity blue = ImmutableActivity.builder() + .name(as.getName() + " Blue") + .color(Color.BLUE) + .build(); + blue.addInterval(new Interval(0.35, 0.44)); + blue.addInterval(new Interval(0.6, 0.95)); + as.addActivity(blue); - Activity circle = - ImmutableActivity.builder() - .name(as.getName() + " Circle") - .color(Color.BLUE) - .symbol(new Circle().setFill(true).setSize(8)) - .build(); - circle.addInterval(new Interval(0.30, 0.30)); - as.addActivity(circle); - Activity diamond = - ImmutableActivity.builder() - .name(as.getName() + " Diamond") - .color(Color.MAGENTA) - .symbol(new Square().setFill(true).setSize(8).setRotate(45)) - .build(); - diamond.addInterval(new Interval(0.15, 0.15001)); // draw two overlapping symbols - diamond.addInterval(new Interval(0.22, 0.24)); // draw two symbols - as.addActivity(diamond); - activities.add(as); + Activity circle = ImmutableActivity.builder() + .name(as.getName() + " Circle") + .color(Color.BLUE) + .symbol(new Circle().setFill(true).setSize(8)) + .build(); + circle.addInterval(new Interval(0.30, 0.30)); + as.addActivity(circle); + Activity diamond = ImmutableActivity.builder() + .name(as.getName() + " Diamond") + .color(Color.MAGENTA) + .symbol(new Square().setFill(true).setSize(8).setRotate(45)) + .build(); + diamond.addInterval(new Interval(0.15, 0.15001)); // draw two overlapping symbols + diamond.addInterval(new Interval(0.22, 0.24)); // draw two symbols + as.addActivity(diamond); + activities.add(as); - as = new ActivitySet("Green"); - Activity green = - ImmutableActivity.builder().name(as.getName() + " Green").color(Color.GREEN).build(); - green.addInterval(new Interval(-1, 1)); - as.addActivity(green); - activities.add(as); + as = new ActivitySet("Green"); + Activity green = ImmutableActivity.builder() + .name(as.getName() + " Green") + .color(Color.GREEN) + .build(); + green.addInterval(new Interval(-1, 1)); + as.addActivity(green); + activities.add(as); - PlotConfig config = - ImmutablePlotConfig.builder() - .width(800) - .height(600) - .rightMargin(220) - .title("Activity Plot Example") - .build(); + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .rightMargin(220) + .title("Activity Plot Example") + .build(); - config = - ImmutablePlotConfig.builder() - .from(config) - .legendPosition( - new Point2D.Double(config.getRightPlotEdge() + 20, config.topMargin() + 50)) - .legendFont(new Font("Helvetica", Font.BOLD, 18)) - .legendColor(true) - .build(); + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.getRightPlotEdge() + 20, config.topMargin() + 50)) + .legendFont(new Font("Helvetica", Font.BOLD, 18)) + .legendColor(true) + .build(); - AxisX xLowerAxis = new AxisX(0, 1, "X Axis"); - xLowerAxis.setMinorTicks(null); + AxisX xLowerAxis = new AxisX(0, 1, "X Axis"); + xLowerAxis.setMinorTicks(null); - AxisY yLeftAxis = new AxisY(-1, 1, " "); - yLeftAxis.setMinorTicks(null); + AxisY yLeftAxis = new AxisY(-1, 1, " "); + yLeftAxis.setMinorTicks(null); - ActivityPlot canvas = new ActivityPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); - canvas.plot(activities); + ActivityPlot canvas = new ActivityPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + canvas.plot(activities); - Set legendEntries = new LinkedHashSet<>(); - for (ActivitySet activitySet : activities) { - for (Activity a : activitySet.getActivityMap().values()) { - legendEntries.add(a.getLegendEntry()); - } + Set legendEntries = new LinkedHashSet<>(); + for (ActivitySet activitySet : activities) { + for (Activity a : activitySet.getActivityMap().values()) { + legendEntries.add(a.getLegendEntry()); + } + } + for (LegendEntry le : legendEntries) canvas.addToLegend(le); + canvas.drawLegend(); + + return canvas.getImage(); } - for (LegendEntry le : legendEntries) canvas.addToLegend(le); - canvas.drawLegend(); - return canvas.getImage(); - } - - public static void main(String[] args) { - BufferedImage image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); -// PlotCanvas.writeImage("doc/images/activityPlotDemo.png", image); - } + public static void main(String[] args) { + BufferedImage image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("doc/images/activityPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/AreaPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/AreaPlotDemo.java index e20426f..009e330 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/AreaPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/AreaPlotDemo.java @@ -40,139 +40,155 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class AreaPlotDemo { - public static BufferedImage mandelbrot(Interval xRange, Interval yRange) { + public static BufferedImage mandelbrot(Interval xRange, Interval yRange) { - MultivariateFunction func = point -> { - double iter = 0; + MultivariateFunction func = point -> { + double iter = 0; - double re = point[0]; - double im = point[1]; + double re = point[0]; + double im = point[1]; - while (iter < 256) { + while (iter < 256) { - double sqre = re * re - im * im + point[0]; - double sqim = 2 * re * im + point[1]; + double sqre = re * re - im * im + point[0]; + double sqim = 2 * re * im + point[1]; - re = sqre; - im = sqim; + re = sqre; + im = sqim; - if (Math.sqrt(re * re + im * im) > 2) - break; + if (Math.sqrt(re * re + im * im) > 2) break; - iter++; + iter++; + } + return iter; + }; - } - return iter; - }; + PlotConfig config = ImmutablePlotConfig.builder().height(1200).build(); + config = ImmutablePlotConfig.builder() + .from(config) + .width(3 * config.height() / 2) + .title(String.format("Center %f %f", xRange.getBarycenter(), yRange.getBarycenter())) + .build(); - PlotConfig config = ImmutablePlotConfig.builder().height(1200).build(); - config = ImmutablePlotConfig.builder().from(config).width(3 * config.height() / 2) - .title(String.format("Center %f %f", xRange.getBarycenter(), yRange.getBarycenter())).build(); + AxisX xLowerAxis = new AxisX(xRange.getInf(), xRange.getSup(), "Re(c)"); + AxisY yLeftAxis = new AxisY(yRange.getInf(), yRange.getSup(), "Im(c)"); - AxisX xLowerAxis = new AxisX(xRange.getInf(), xRange.getSup(), "Re(c)"); - AxisY yLeftAxis = new AxisY(yRange.getInf(), yRange.getSup(), "Im(c)"); + ColorRamp ramp = ColorRamp.createLinear(0, 256).addLimitColors().createReverse(); - ColorRamp ramp = ColorRamp.createLinear(0, 256).addLimitColors().createReverse(); + AreaPlot canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); - AreaPlot canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); + canvas.plot(func, ramp, xLowerAxis, yLeftAxis); - canvas.plot(func, ramp, xLowerAxis, yLeftAxis); - - return canvas.getImage(); - } - - public static BufferedImage makePlot() { - PlotConfig config = - ImmutablePlotConfig.builder().width(800).height(600).title("cos(X) * cos(Y)").build(); - - AxisX xLowerAxis = new AxisX(-180, 180, "X (degrees)"); - AxisY yLeftAxis = new AxisY(-180, 180, "Y (degrees)"); - - MultivariateFunction func = point -> { - // if (point[0] * point[1] > 0) - // return Double.NaN; - return Math.cos(Math.toRadians(point[0])) * Math.cos(Math.toRadians(point[1])); - }; - - ColorRamp ramp = ColorRamp.createLinear(-1, 1); - AreaPlot canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); - - canvas.plot(func, ramp, xLowerAxis, yLeftAxis); - - // Horizontal, across the top - canvas.drawColorBar( - ImmutableColorBar.builder().rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) - .ramp(ramp).numTicks(5).tickFunction(StringFunctions.fixedFormat("%.1f")).build()); - - // Vertical, along right side - canvas.drawColorBar(ImmutableColorBar.builder() - .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) - .ramp(ramp).numTicks(9).tickFunction(StringFunctions.fixedFormat("%.2f")).build()); - - return canvas.getImage(); - } - - public static BufferedImage plotArray(boolean log) { - - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).title("X * Y").build(); - - double[][] array = new double[101][101]; - int nX = array[0].length; - int nY = array.length; - for (int row = 0; row < nY; row++) { - for (int col = 0; col < nX; col++) { - array[row][col] = row * col; - } + return canvas.getImage(); } - AxisX xLowerAxis = new AxisX(0, nX - 1, "X (pixel)", "%.2f"); - AxisY yLeftAxis = new AxisY(0, nY - 1, "Y (pixel)", "%.2f"); + public static BufferedImage makePlot() { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("cos(X) * cos(Y)") + .build(); - MultivariateFunction func = point -> log ? Math.log10(array[(int) point[1]][(int) point[0]]) - : array[(int) point[1]][(int) point[0]]; - ColorRamp ramp = - ColorRamp.createLinear(0, log ? Math.log10(array[nX - 1][nY - 1]) : array[nX - 1][nY - 1]); - ColorBar colorBar = ImmutableColorBar.builder() - .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)).ramp(ramp).numTicks(5) - .tickFunction(StringFunctions.fixedFormat("%.0f")).log(true).build(); + AxisX xLowerAxis = new AxisX(-180, 180, "X (degrees)"); + AxisY yLeftAxis = new AxisY(-180, 180, "Y (degrees)"); - AreaPlot canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); + MultivariateFunction func = point -> { + // if (point[0] * point[1] > 0) + // return Double.NaN; + return Math.cos(Math.toRadians(point[0])) * Math.cos(Math.toRadians(point[1])); + }; - canvas.plot(func, ramp, xLowerAxis, yLeftAxis); + ColorRamp ramp = ColorRamp.createLinear(-1, 1); + AreaPlot canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); - canvas.drawColorBar(colorBar); + canvas.plot(func, ramp, xLowerAxis, yLeftAxis); - return canvas.getImage(); - } + // Horizontal, across the top + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.1f")) + .build()); - public static void main(String[] args) { + // Vertical, along right side + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) + .ramp(ramp) + .numTicks(9) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build()); - final double xCenter = -0.5; - final double yCenter = 0; - double width = 3; - double height = 2 * width / 3; + return canvas.getImage(); + } - Interval xRange = new Interval(xCenter - width / 2, xCenter + width / 2); - Interval yRange = new Interval(yCenter - height / 2, yCenter + height / 2); + public static BufferedImage plotArray(boolean log) { - BufferedImage image = mandelbrot(xRange, yRange); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("X * Y") + .build(); - image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); + double[][] array = new double[101][101]; + int nX = array[0].length; + int nY = array.length; + for (int row = 0; row < nY; row++) { + for (int col = 0; col < nX; col++) { + array[row][col] = row * col; + } + } - image = plotArray(true); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); - PlotCanvas.writeImage("doc/images/areaPlotDemo.png", image); - } + AxisX xLowerAxis = new AxisX(0, nX - 1, "X (pixel)", "%.2f"); + AxisY yLeftAxis = new AxisY(0, nY - 1, "Y (pixel)", "%.2f"); + MultivariateFunction func = point -> + log ? Math.log10(array[(int) point[1]][(int) point[0]]) : array[(int) point[1]][(int) point[0]]; + ColorRamp ramp = ColorRamp.createLinear(0, log ? Math.log10(array[nX - 1][nY - 1]) : array[nX - 1][nY - 1]); + ColorBar colorBar = ImmutableColorBar.builder() + .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.0f")) + .log(true) + .build(); + + AreaPlot canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + + canvas.plot(func, ramp, xLowerAxis, yLeftAxis); + + canvas.drawColorBar(colorBar); + + return canvas.getImage(); + } + + public static void main(String[] args) { + + final double xCenter = -0.5; + final double yCenter = 0; + double width = 3; + double height = 2 * width / 3; + + Interval xRange = new Interval(xCenter - width / 2, xCenter + width / 2); + Interval yRange = new Interval(yCenter - height / 2, yCenter + height / 2); + + BufferedImage image = mandelbrot(xRange, yRange); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + + image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + + image = plotArray(true); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + PlotCanvas.writeImage("doc/images/areaPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/BarPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/BarPlotDemo.java index 2a111f0..18e36ef 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/BarPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/BarPlotDemo.java @@ -45,24 +45,34 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class BarPlotDemo { public static BufferedImage plotGaussianHistogram() { - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).title("Gaussian Histogram").build(); + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Gaussian Histogram") + .build(); - config = ImmutablePlotConfig.builder().from(config).legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)).legendFont(new Font(Font.MONOSPACED, Font.BOLD, 18)).gridColor(Color.LIGHT_GRAY).build(); + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) + .legendFont(new Font(Font.MONOSPACED, Font.BOLD, 18)) + .gridColor(Color.LIGHT_GRAY) + .build(); AxisX xLowerAxis = new AxisX(-4, 4, "X Axis"); double binSize = 0.2; List binBoundaries = new ArrayList<>(); binBoundaries.add(xLowerAxis.getRange().getMin() - binSize / 2); - for (double x = xLowerAxis.getRange().getMin(); x <= xLowerAxis.getRange().getMax(); x += binSize) { + for (double x = xLowerAxis.getRange().getMin(); + x <= xLowerAxis.getRange().getMax(); + x += binSize) { binBoundaries.add(x + binSize / 2); } binBoundaries.add(xLowerAxis.getRange().getMax() + binSize / 2); HistogramDataSet data = new HistogramDataSet("Histogram", binBoundaries); int npts = 10000; - for (int i = 0; i < npts; i++) - data.add(new Random().nextGaussian()); + for (int i = 0; i < npts; i++) data.add(new Random().nextGaussian()); data = data.getNormalized("Normalized"); @@ -77,11 +87,26 @@ public class BarPlotDemo { List yValues = data.getYValues(); DescriptiveStatistics stats = new DescriptiveStatistics(); for (double y : yValues) stats.addValue(y); - canvas.addToLegend(ImmutableLegendEntry.builder().name(String.format("N %d", stats.getN())).color(data.getColor()).build()); - canvas.addToLegend(ImmutableLegendEntry.builder().name(String.format("min %g", stats.getMin())).color(data.getColor()).build()); - canvas.addToLegend(ImmutableLegendEntry.builder().name(String.format("max %g", stats.getMax())).color(data.getColor()).build()); - canvas.addToLegend(ImmutableLegendEntry.builder().name(String.format("mean %g", stats.getMean())).color(data.getColor()).build()); - canvas.addToLegend(ImmutableLegendEntry.builder().name(String.format("std dev %g", stats.getStandardDeviation())).color(data.getColor()).build()); + canvas.addToLegend(ImmutableLegendEntry.builder() + .name(String.format("N %d", stats.getN())) + .color(data.getColor()) + .build()); + canvas.addToLegend(ImmutableLegendEntry.builder() + .name(String.format("min %g", stats.getMin())) + .color(data.getColor()) + .build()); + canvas.addToLegend(ImmutableLegendEntry.builder() + .name(String.format("max %g", stats.getMax())) + .color(data.getColor()) + .build()); + canvas.addToLegend(ImmutableLegendEntry.builder() + .name(String.format("mean %g", stats.getMean())) + .color(data.getColor()) + .build()); + canvas.addToLegend(ImmutableLegendEntry.builder() + .name(String.format("std dev %g", stats.getStandardDeviation())) + .color(data.getColor()) + .build()); canvas.drawGrid(); canvas.drawLegend(); @@ -97,9 +122,18 @@ public class BarPlotDemo { public static BufferedImage plotCosX() { - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).title("Bar Plot Example").build(); + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Bar Plot Example") + .build(); - config = ImmutablePlotConfig.builder().from(config).legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)).legendFont(new Font("Helvetica", Font.BOLD, 36)).gridColor(Color.LIGHT_GRAY).build(); + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) + .legendFont(new Font("Helvetica", Font.BOLD, 36)) + .gridColor(Color.LIGHT_GRAY) + .build(); AxisX xLowerAxis = new AxisX(0, 2 * Math.PI, "X Axis"); AxisY yLeftAxis = new AxisY(-1.5, 1.5, "Y Axis"); @@ -131,7 +165,6 @@ public class BarPlotDemo { image = plotGaussianHistogram(); PlotUtils.addCreationDate(image); PlotCanvas.showJFrame(image); - PlotCanvas.writeImage("doc/images/barPlotDemo.png", image); + PlotCanvas.writeImage("doc/images/barPlotDemo.png", image); } - } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/CSVPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/CSVPlotDemo.java index e425897..558d41b 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/CSVPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/CSVPlotDemo.java @@ -35,59 +35,59 @@ import terrasaur.utils.saaPlotLib.data.DiscreteDataSet; public class CSVPlotDemo { - private final Map> data; + private final Map> data; - public CSVPlotDemo(List lines) { - this.data = new HashMap<>(); + public CSVPlotDemo(List lines) { + this.data = new HashMap<>(); - for (String line : lines) { - if (line.trim().isEmpty() || line.trim().startsWith("#")) - continue; - String[] parts = line.split(","); - for (int i = 0; i < parts.length; i++) { - List thisColumn = data.computeIfAbsent(i, k -> new ArrayList<>()); - try { - thisColumn.add(Double.parseDouble(parts[i].trim())); - } catch (NumberFormatException e) { - System.err.println("Parse error " + e.getLocalizedMessage()); + for (String line : lines) { + if (line.trim().isEmpty() || line.trim().startsWith("#")) continue; + String[] parts = line.split(","); + for (int i = 0; i < parts.length; i++) { + List thisColumn = data.computeIfAbsent(i, k -> new ArrayList<>()); + try { + thisColumn.add(Double.parseDouble(parts[i].trim())); + } catch (NumberFormatException e) { + System.err.println("Parse error " + e.getLocalizedMessage()); + } + } } - } - } - } - - public BufferedImage plot(String xTitle, int xCol, String yTitle, int yCol, String title) { - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).title(title).build(); - - DiscreteDataSet thisData = new DiscreteDataSet(yTitle); - thisData.add(data.get(xCol), data.get(yCol)); - - DiscreteDataPlot plot = new DiscreteDataPlot(config); - plot.setAxes(thisData.defaultXAxis(xTitle), thisData.defaultYAxis(yTitle)); - plot.drawAxes(); - plot.plot(thisData); - - return plot.getImage(); - } - - public static void test() { - - List lines = new ArrayList<>(); - for (int i = 0; i < 314; i++) { - double angle = i * 0.02; - lines.add( - String.format("%f,%f,%f,%f", angle, Math.sin(angle), Math.cos(angle), Math.tan(angle))); } - CSVPlotDemo csv = new CSVPlotDemo(lines); + public BufferedImage plot(String xTitle, int xCol, String yTitle, int yCol, String title) { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title(title) + .build(); - PlotCanvas.showJFrame(csv.plot("Angle", 0, "Sin", 1, "Sin Plot")); - PlotCanvas.showJFrame(csv.plot("Angle", 0, "Cos", 2, "Cos Plot")); - PlotCanvas.showJFrame(csv.plot("Angle", 0, "Tan", 3, "Tan Plot")); + DiscreteDataSet thisData = new DiscreteDataSet(yTitle); + thisData.add(data.get(xCol), data.get(yCol)); - } + DiscreteDataPlot plot = new DiscreteDataPlot(config); + plot.setAxes(thisData.defaultXAxis(xTitle), thisData.defaultYAxis(yTitle)); + plot.drawAxes(); + plot.plot(thisData); - public static void main(String[] args) { - test(); - } + return plot.getImage(); + } + public static void test() { + + List lines = new ArrayList<>(); + for (int i = 0; i < 314; i++) { + double angle = i * 0.02; + lines.add(String.format("%f,%f,%f,%f", angle, Math.sin(angle), Math.cos(angle), Math.tan(angle))); + } + + CSVPlotDemo csv = new CSVPlotDemo(lines); + + PlotCanvas.showJFrame(csv.plot("Angle", 0, "Sin", 1, "Sin Plot")); + PlotCanvas.showJFrame(csv.plot("Angle", 0, "Cos", 2, "Cos Plot")); + PlotCanvas.showJFrame(csv.plot("Angle", 0, "Tan", 3, "Tan Plot")); + } + + public static void main(String[] args) { + test(); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/ColorRampDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/ColorRampDemo.java index e7cfdde..ef5a4e1 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/ColorRampDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/ColorRampDemo.java @@ -43,69 +43,66 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class ColorRampDemo { - public static BufferedImage getRampImage() { - PlotConfig config = - ImmutablePlotConfig.builder() - .width(1800) - .height(1000) - .topMargin(0) - .leftMargin(0) - .rightMargin(0) - .bottomMargin(0) - .build(); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); + public static BufferedImage getRampImage() { + PlotConfig config = ImmutablePlotConfig.builder() + .width(1800) + .height(1000) + .topMargin(0) + .leftMargin(0) + .rightMargin(0) + .bottomMargin(0) + .build(); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); - int x = 20; - int y = 20; - int width = 200; - int height = 25; + int x = 20; + int y = 20; + int width = 200; + int height = 25; - AxisX xAxis = new AxisX(0, config.width(), ""); - AxisY yAxis = new AxisY(0, config.height(), ""); - canvas.setAxes(xAxis, yAxis); + AxisX xAxis = new AxisX(0, config.width(), ""); + AxisY yAxis = new AxisY(0, config.height(), ""); + canvas.setAxes(xAxis, yAxis); - for (ColorTable.FAMILY f : ColorTable.FAMILY.values()) { - Annotation a = - ImmutableAnnotation.builder() - .text(f.name()) - .color(Color.BLACK) - .font(new Font("HELVETICA", Font.BOLD, 24)) - .horizontalAlignment(Keyword.ALIGN_LEFT) - .verticalAlignment(Keyword.ALIGN_TOP) - .build(); + for (ColorTable.FAMILY f : ColorTable.FAMILY.values()) { + Annotation a = ImmutableAnnotation.builder() + .text(f.name()) + .color(Color.BLACK) + .font(new Font("HELVETICA", Font.BOLD, 24)) + .horizontalAlignment(Keyword.ALIGN_LEFT) + .verticalAlignment(Keyword.ALIGN_TOP) + .build(); - canvas.addAnnotation(a, x, config.height() - y); - x += 50; - y += 50; + canvas.addAnnotation(a, x, config.height() - y); + x += 50; + y += 50; - for (ColorRamp.TYPE type : ColorRamp.TYPE.values()) { - if (ColorRamp.TYPE.isType(f).test(type)) { - ColorRamp ramp = ColorRamp.create(type, 0, 1).addTitle(type.name()); + for (ColorRamp.TYPE type : ColorRamp.TYPE.values()) { + if (ColorRamp.TYPE.isType(f).test(type)) { + ColorRamp ramp = ColorRamp.create(type, 0, 1).addTitle(type.name()); - ColorBar cb = - ImmutableColorBar.builder() - .rect(new Rectangle(x, y, width, height)) - .ramp(ramp) - .numTicks(5) - .tickFunction(StringFunctions.fixedFormat("%.2f")) - .build(); - canvas.drawColorBar(cb); + ColorBar cb = ImmutableColorBar.builder() + .rect(new Rectangle(x, y, width, height)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build(); + canvas.drawColorBar(cb); - x += (int) Math.round(1.5 * width); - if (x > config.width() - width) { - x = 70; + x += (int) Math.round(1.5 * width); + if (x > config.width() - width) { + x = 70; + y += 3 * height; + } + } + } + x = 20; y += 3 * height; - } } - } - x = 20; - y += 3 * height; + + return canvas.getImage(); } - return canvas.getImage(); - } - - public static void main(String[] args) { - PlotCanvas.showJFrame(getRampImage()); - } + public static void main(String[] args) { + PlotCanvas.showJFrame(getRampImage()); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/LinePlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/LinePlotDemo.java index 86eba48..19649e3 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/LinePlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/LinePlotDemo.java @@ -47,160 +47,158 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class LinePlotDemo { - private static DiscreteDataSet createSinX(AxisRange range) { - DiscreteDataSet ds = new DiscreteDataSet("sin"); - for (int i = 0; i < 100; i++) { - double x = range.getMin() + range.getLength() * i / 99.; + private static DiscreteDataSet createSinX(AxisRange range) { + DiscreteDataSet ds = new DiscreteDataSet("sin"); + for (int i = 0; i < 100; i++) { + double x = range.getMin() + range.getLength() * i / 99.; - /*- - Only the Y coordinate has error bars. The error bar limits are sin(x)-0.05 and sin(x)+0.1. - The Z coordinate is ignored for a line plot. - x is also given as the property value. This is used for the point's color. - */ - ds.add( - new Point3D(x, Double.NaN, Double.NaN), - new Point3D(Math.sin(x), 0.05, 0.1), - new Point3D(Double.NaN, Double.NaN, Double.NaN), - x); - } - return ds; - } - - private static DiscreteDataSet createCosX(AxisRange range) { - DiscreteDataSet ds = new DiscreteDataSet("cos"); - for (int i = 0; i < 100; i++) { - - // random X coordinates - double x = new Random().nextDouble() * range.getLength() + range.getMin(); - double y = Math.cos(x); - ds.add(x, y); + /*- + Only the Y coordinate has error bars. The error bar limits are sin(x)-0.05 and sin(x)+0.1. + The Z coordinate is ignored for a line plot. + x is also given as the property value. This is used for the point's color. + */ + ds.add( + new Point3D(x, Double.NaN, Double.NaN), + new Point3D(Math.sin(x), 0.05, 0.1), + new Point3D(Double.NaN, Double.NaN, Double.NaN), + x); + } + return ds; } - // sort X coordinates - ds.getData().sort(COORDINATE.X); + private static DiscreteDataSet createCosX(AxisRange range) { + DiscreteDataSet ds = new DiscreteDataSet("cos"); + for (int i = 0; i < 100; i++) { - return ds; - } + // random X coordinates + double x = new Random().nextDouble() * range.getLength() + range.getMin(); + double y = Math.cos(x); + ds.add(x, y); + } - private static DiscreteDataSet createExpX(AxisRange range) { - DiscreteDataSet ds = new DiscreteDataSet("exp"); - for (int i = 0; i < 100; i++) { - double x = range.getMin() + range.getLength() * i / 99.; - double y = Math.exp(x); - ds.add(x, y); + // sort X coordinates + ds.getData().sort(COORDINATE.X); + + return ds; } - return ds; - } - public static BufferedImage makeSimplePlot() { - AxisX xAxis = new AxisX(-1, 10, "X Axis"); - AxisY yAxis = new AxisY(0.1, 1e5, "Y Axis", "%.4g"); - yAxis.setLog(true); - DiscreteDataSet cos = createExpX(xAxis.getRange()); - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).build(); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - canvas.setAxes(xAxis, yAxis); - canvas.drawAxes(); - canvas.plot(cos); - return canvas.getImage(); - } + private static DiscreteDataSet createExpX(AxisRange range) { + DiscreteDataSet ds = new DiscreteDataSet("exp"); + for (int i = 0; i < 100; i++) { + double x = range.getMin() + range.getLength() * i / 99.; + double y = Math.exp(x); + ds.add(x, y); + } + return ds; + } - public static BufferedImage makePlot() { - PlotConfig config = - ImmutablePlotConfig.builder() - .width(800) - .height(600) - .topMargin(120) - .rightMargin(120) - .title("Line Plot Example") - .build(); + public static BufferedImage makeSimplePlot() { + AxisX xAxis = new AxisX(-1, 10, "X Axis"); + AxisY yAxis = new AxisY(0.1, 1e5, "Y Axis", "%.4g"); + yAxis.setLog(true); + DiscreteDataSet cos = createExpX(xAxis.getRange()); + PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).build(); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + canvas.setAxes(xAxis, yAxis); + canvas.drawAxes(); + canvas.plot(cos); + return canvas.getImage(); + } - config = - ImmutablePlotConfig.builder() - .from(config) - .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) - .legendFont(new Font("Helvetica", Font.BOLD, 36)) - .gridColor(Color.LIGHT_GRAY) - .build(); + public static BufferedImage makePlot() { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .topMargin(120) + .rightMargin(120) + .title("Line Plot Example") + .build(); - AxisX xLowerAxis = new AxisX(0, 2 * Math.PI, "X Axis"); - AxisX xUpperAxis = new AxisX(-Math.PI, Math.PI, "Top Title"); - xLowerAxis.setRotateLabels(-Math.PI / 4); + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) + .legendFont(new Font("Helvetica", Font.BOLD, 36)) + .gridColor(Color.LIGHT_GRAY) + .build(); - AxisY yLeftAxis = new AxisY(-1, 1, "Y Axis"); - AxisY yRightAxis = new AxisY(-1, 1, "Right Title"); - yRightAxis.setRotateTitle(Math.PI / 2); + AxisX xLowerAxis = new AxisX(0, 2 * Math.PI, "X Axis"); + AxisX xUpperAxis = new AxisX(-Math.PI, Math.PI, "Top Title"); + xLowerAxis.setRotateLabels(-Math.PI / 4); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis, xUpperAxis, yRightAxis); + AxisY yLeftAxis = new AxisY(-1, 1, "Y Axis"); + AxisY yRightAxis = new AxisY(-1, 1, "Right Title"); + yRightAxis.setRotateTitle(Math.PI / 2); - DiscreteDataSet sin = createSinX(xLowerAxis.getRange()); - DiscreteDataSet cos = createCosX(xUpperAxis.getRange()); - float[] dash = {10.0f}; - cos.setStroke( - new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f)); - cos.setColor(Color.RED); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis, xUpperAxis, yRightAxis); - xLowerAxis.setAxisColor(sin.getColor()); - xUpperAxis.setAxisColor(cos.getColor()); + DiscreteDataSet sin = createSinX(xLowerAxis.getRange()); + DiscreteDataSet cos = createCosX(xUpperAxis.getRange()); + float[] dash = {10.0f}; + cos.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f)); + cos.setColor(Color.RED); - canvas.drawAxes(); - canvas.drawGrid(); - canvas.plot(sin); - sin.setSymbol(new Square().setFill(true).setSize(4).setRotate(45)); + xLowerAxis.setAxisColor(sin.getColor()); + xUpperAxis.setAxisColor(cos.getColor()); - // give each symbol a different color based on its x value - ColorRamp ramp = ColorRamp.createHue(sin.getXStats().getMin(), sin.getXStats().getMax()); - sin.setColorRamp(ramp); + canvas.drawAxes(); + canvas.drawGrid(); + canvas.plot(sin); + sin.setSymbol(new Square().setFill(true).setSize(4).setRotate(45)); - canvas.plot(sin); - canvas.plot(cos, xUpperAxis); - canvas.addToLegend(sin.getLegendEntry()); - canvas.addToLegend(cos.getLegendEntry()); - canvas.drawLegend(); + // give each symbol a different color based on its x value + ColorRamp ramp = + ColorRamp.createHue(sin.getXStats().getMin(), sin.getXStats().getMax()); + sin.setColorRamp(ramp); - Annotations a = new Annotations(); - a.addAnnotation(ImmutableAnnotation.builder().text("default").build(), Math.PI / 2, 0); - a.addAnnotation( - ImmutableAnnotation.builder().text("blue").color(Color.BLUE).build(), Math.PI / 2, 0.25); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("small italic green") - .color(Color.GREEN) - .font(new Font("Helvetica", Font.ITALIC, 6)) - .build(), - Math.PI / 2, - -0.25); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("left") - .horizontalAlignment(Keyword.ALIGN_RIGHT) - .verticalAlignment(Keyword.ALIGN_CENTER) - .build(), - Math.PI / 2, - -0.5); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("right") - .horizontalAlignment(Keyword.ALIGN_LEFT) - .verticalAlignment(Keyword.ALIGN_CENTER) - .build(), - Math.PI / 2, - -0.5); + canvas.plot(sin); + canvas.plot(cos, xUpperAxis); + canvas.addToLegend(sin.getLegendEntry()); + canvas.addToLegend(cos.getLegendEntry()); + canvas.drawLegend(); - canvas.addAnnotations(a); + Annotations a = new Annotations(); + a.addAnnotation(ImmutableAnnotation.builder().text("default").build(), Math.PI / 2, 0); + a.addAnnotation( + ImmutableAnnotation.builder().text("blue").color(Color.BLUE).build(), Math.PI / 2, 0.25); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("small italic green") + .color(Color.GREEN) + .font(new Font("Helvetica", Font.ITALIC, 6)) + .build(), + Math.PI / 2, + -0.25); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("left") + .horizontalAlignment(Keyword.ALIGN_RIGHT) + .verticalAlignment(Keyword.ALIGN_CENTER) + .build(), + Math.PI / 2, + -0.5); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("right") + .horizontalAlignment(Keyword.ALIGN_LEFT) + .verticalAlignment(Keyword.ALIGN_CENTER) + .build(), + Math.PI / 2, + -0.5); - return canvas.getImage(); - } + canvas.addAnnotations(a); - public static void main(String[] args) { - BufferedImage image = makeSimplePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); + return canvas.getImage(); + } - image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); -// PlotCanvas.writeImage("doc/images/linePlotDemo.png", image); - } + public static void main(String[] args) { + BufferedImage image = makeSimplePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + + image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("doc/images/linePlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/MapPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/MapPlotDemo.java index 32693b1..8f36185 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/MapPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/MapPlotDemo.java @@ -61,252 +61,241 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class MapPlotDemo { - private static final Logger logger = LogManager.getLogger(MapPlotDemo.class); + private static final Logger logger = LogManager.getLogger(MapPlotDemo.class); - private static List get180Block() { - List block = new ArrayList<>(); - block.add(new Point2D.Double(Math.toRadians(-175), Math.toRadians(25))); - block.add(new Point2D.Double(Math.toRadians(-170), Math.toRadians(25))); - block.add(new Point2D.Double(Math.toRadians(175), Math.toRadians(-25))); - block.add(new Point2D.Double(Math.toRadians(170), Math.toRadians(-25))); - return block; - } + private static List get180Block() { + List block = new ArrayList<>(); + block.add(new Point2D.Double(Math.toRadians(-175), Math.toRadians(25))); + block.add(new Point2D.Double(Math.toRadians(-170), Math.toRadians(25))); + block.add(new Point2D.Double(Math.toRadians(175), Math.toRadians(-25))); + block.add(new Point2D.Double(Math.toRadians(170), Math.toRadians(-25))); + return block; + } - private static List get170Block() { - List block = new ArrayList<>(); - block.add(new Point2D.Double(Math.toRadians(169), Math.toRadians(15))); - block.add(new Point2D.Double(Math.toRadians(171), Math.toRadians(15))); - block.add(new Point2D.Double(Math.toRadians(171), Math.toRadians(-5))); - block.add(new Point2D.Double(Math.toRadians(169), Math.toRadians(-5))); - return block; - } + private static List get170Block() { + List block = new ArrayList<>(); + block.add(new Point2D.Double(Math.toRadians(169), Math.toRadians(15))); + block.add(new Point2D.Double(Math.toRadians(171), Math.toRadians(15))); + block.add(new Point2D.Double(Math.toRadians(171), Math.toRadians(-5))); + block.add(new Point2D.Double(Math.toRadians(169), Math.toRadians(-5))); + return block; + } - private static List get190Block() { - List block = new ArrayList<>(); - block.add(new Point2D.Double(Math.toRadians(189), Math.toRadians(15))); - block.add(new Point2D.Double(Math.toRadians(191), Math.toRadians(15))); - block.add(new Point2D.Double(Math.toRadians(191), Math.toRadians(-5))); - block.add(new Point2D.Double(Math.toRadians(189), Math.toRadians(-5))); - return block; - } + private static List get190Block() { + List block = new ArrayList<>(); + block.add(new Point2D.Double(Math.toRadians(189), Math.toRadians(15))); + block.add(new Point2D.Double(Math.toRadians(191), Math.toRadians(15))); + block.add(new Point2D.Double(Math.toRadians(191), Math.toRadians(-5))); + block.add(new Point2D.Double(Math.toRadians(189), Math.toRadians(-5))); + return block; + } - private static DiscreteDataSet LAXtoSYD() { - DiscreteDataSet ds = new DiscreteDataSet(""); - ds.add(Math.toRadians(-118), Math.toRadians(34)); - ds.add(Math.toRadians(151), Math.toRadians(-34)); - ds.setColor(Color.CYAN); - return ds; - } + private static DiscreteDataSet LAXtoSYD() { + DiscreteDataSet ds = new DiscreteDataSet(""); + ds.add(Math.toRadians(-118), Math.toRadians(34)); + ds.add(Math.toRadians(151), Math.toRadians(-34)); + ds.setColor(Color.CYAN); + return ds; + } - private static GeneralPath getAustraliaMask() { - // draw a shape over Australia - Map oz = new LinkedHashMap<>(); - oz.put(142.45, -11.1497); - oz.put(153.31, -27.2056); - oz.put(146.43, -39.0426); - oz.put(131.13, -31.853); - oz.put(115.02, -34.402); - oz.put(113.83, -22.145); - oz.put(121.26, -19.298); - oz.put(132.77, -11.574); - oz.put(137.00, -12.30); - oz.put(135.52, -14.90); - oz.put(140.71, -17.53); + private static GeneralPath getAustraliaMask() { + // draw a shape over Australia + Map oz = new LinkedHashMap<>(); + oz.put(142.45, -11.1497); + oz.put(153.31, -27.2056); + oz.put(146.43, -39.0426); + oz.put(131.13, -31.853); + oz.put(115.02, -34.402); + oz.put(113.83, -22.145); + oz.put(121.26, -19.298); + oz.put(132.77, -11.574); + oz.put(137.00, -12.30); + oz.put(135.52, -14.90); + oz.put(140.71, -17.53); - Map.Entry first = oz.entrySet().iterator().next(); - GeneralPath gp = new GeneralPath(); - gp.moveTo(first.getKey(), first.getValue()); - for (Double lon : oz.keySet()) gp.lineTo(lon, oz.get(lon)); - gp.closePath(); + Map.Entry first = oz.entrySet().iterator().next(); + GeneralPath gp = new GeneralPath(); + gp.moveTo(first.getKey(), first.getValue()); + for (Double lon : oz.keySet()) gp.lineTo(lon, oz.get(lon)); + gp.closePath(); - return gp; - } + return gp; + } - public static BufferedImage makeRectangular(File baseMap) { - PlotConfig config = - ImmutablePlotConfig.builder() - .width(800) - .height(600) - .title("Map Plot Example") - .leftMargin(120) - .rightMargin(80) - .topMargin(100) - .bottomMargin(80) - .build(); + public static BufferedImage makeRectangular(File baseMap) { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Map Plot Example") + .leftMargin(120) + .rightMargin(80) + .topMargin(100) + .bottomMargin(80) + .build(); - AxisX xLowerAxis = new AxisX(-180, 180, "Longitude (degrees)", "%.0fE"); - AxisY yLeftAxis = new AxisY(-90, 90, "Latitude (degrees)", "%.0f"); + AxisX xLowerAxis = new AxisX(-180, 180, "Longitude (degrees)", "%.0fE"); + AxisY yLeftAxis = new AxisY(-90, 90, "Latitude (degrees)", "%.0f"); - GeneralPath gp = getAustraliaMask(); - /* - * point[0] is longitude in radians, point[1] is latitude in radians. - */ - MultivariateFunction func = - point -> { - // transparent pixel if point is outside the region - if (!gp.contains(Math.toDegrees(point[0]), Math.toDegrees(point[1]))) return Double.NaN; + GeneralPath gp = getAustraliaMask(); + /* + * point[0] is longitude in radians, point[1] is latitude in radians. + */ + MultivariateFunction func = point -> { + // transparent pixel if point is outside the region + if (!gp.contains(Math.toDegrees(point[0]), Math.toDegrees(point[1]))) return Double.NaN; - return Math.cos(point[0]) * Math.cos(point[1]); + return Math.cos(point[0]) * Math.cos(point[1]); }; - // Define the map projection - LatitudinalVector centerPoint = - new LatitudinalVector( - 1, - Math.toRadians(yLeftAxis.getRange().getMiddle()), - Math.toRadians(xLowerAxis.getRange().getMiddle())); - Projection p = new ProjectionRectangular(config.width(), config.height(), centerPoint); + // Define the map projection + LatitudinalVector centerPoint = new LatitudinalVector( + 1, + Math.toRadians(yLeftAxis.getRange().getMiddle()), + Math.toRadians(xLowerAxis.getRange().getMiddle())); + Projection p = new ProjectionRectangular(config.width(), config.height(), centerPoint); - MapPlot canvas = new MapPlot(config, p); - try { - BufferedImage map = ImageIO.read(baseMap); - canvas.setBackgroundMap(map); - } catch (IOException e) { - e.printStackTrace(); + MapPlot canvas = new MapPlot(config, p); + try { + BufferedImage map = ImageIO.read(baseMap); + canvas.setBackgroundMap(map); + } catch (IOException e) { + e.printStackTrace(); + } + + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + + ColorRamp ramp = ColorRamp.createHue(-1, 1); + canvas.plot(func, ramp, xLowerAxis, yLeftAxis); + + canvas.plot(Color.RED, get170Block()); + canvas.plot(Color.GREEN, get180Block()); + canvas.plot(Color.BLUE, get190Block()); + + canvas.plot(LAXtoSYD()); + + Annotations a = new Annotations(); + a.addAnnotation( + ImmutableAnnotation.builder().text("Perth").build(), Math.toRadians(115.82), Math.toRadians(-31.97)); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("Auckland") + .color(Color.ORANGE) + .font(new Font("Times New Roman", Font.BOLD, 24)) + .build(), + Math.toRadians(174.74), + Math.toRadians(-36.840556)); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("Singapore") + .color(Color.WHITE) + .font(new Font("Times New Roman", Font.BOLD, 24)) + .build(), + Math.toRadians(103.9), + Math.toRadians(1.20)); + canvas.drawLatLonGrid(Math.toRadians(30), Math.toRadians(30), true); + canvas.addAnnotations(a); + + return canvas.getImage(); } - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); + public static BufferedImage makeOrthographic(File baseMap) { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Map Plot Example") + .leftMargin(80) + .rightMargin(80) + .topMargin(100) + .bottomMargin(80) + .build(); - ColorRamp ramp = ColorRamp.createHue(-1, 1); - canvas.plot(func, ramp, xLowerAxis, yLeftAxis); + GeneralPath gp = getAustraliaMask(); + /* + * point[0] is longitude in radians, point[1] is latitude in radians. + */ + MultivariateFunction func = point -> { + // transparent pixel if point is outside the region + if (!gp.contains(Math.toDegrees(point[0]), Math.toDegrees(point[1]))) return Double.NaN; - canvas.plot(Color.RED, get170Block()); - canvas.plot(Color.GREEN, get180Block()); - canvas.plot(Color.BLUE, get190Block()); - - canvas.plot(LAXtoSYD()); - - Annotations a = new Annotations(); - a.addAnnotation( - ImmutableAnnotation.builder().text("Perth").build(), - Math.toRadians(115.82), - Math.toRadians(-31.97)); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("Auckland") - .color(Color.ORANGE) - .font(new Font("Times New Roman", Font.BOLD, 24)) - .build(), - Math.toRadians(174.74), - Math.toRadians(-36.840556)); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("Singapore") - .color(Color.WHITE) - .font(new Font("Times New Roman", Font.BOLD, 24)) - .build(), - Math.toRadians(103.9), - Math.toRadians(1.20)); - canvas.drawLatLonGrid(Math.toRadians(30), Math.toRadians(30), true); - canvas.addAnnotations(a); - - return canvas.getImage(); - } - - public static BufferedImage makeOrthographic(File baseMap) { - PlotConfig config = - ImmutablePlotConfig.builder() - .width(800) - .height(600) - .title("Map Plot Example") - .leftMargin(80) - .rightMargin(80) - .topMargin(100) - .bottomMargin(80) - .build(); - - GeneralPath gp = getAustraliaMask(); - /* - * point[0] is longitude in radians, point[1] is latitude in radians. - */ - MultivariateFunction func = - point -> { - // transparent pixel if point is outside the region - if (!gp.contains(Math.toDegrees(point[0]), Math.toDegrees(point[1]))) return Double.NaN; - - return Math.cos(point[0]) * Math.cos(point[1]); + return Math.cos(point[0]) * Math.cos(point[1]); }; - // Define the map projection - LatitudinalVector centerPoint = - new LatitudinalVector(1, Math.toRadians(-30), Math.toRadians(140)); + // Define the map projection + LatitudinalVector centerPoint = new LatitudinalVector(1, Math.toRadians(-30), Math.toRadians(140)); - // Note the Y axis won't be right for this projection, but we'll draw it anyway - ProjectionOrthographic p = - new ProjectionOrthographic(config.width(), config.height(), centerPoint); - p.setRadius(1); + // Note the Y axis won't be right for this projection, but we'll draw it anyway + ProjectionOrthographic p = new ProjectionOrthographic(config.width(), config.height(), centerPoint); + p.setRadius(1); - MapPlot canvas = new MapPlot(config, p); - try { - BufferedImage map = ImageIO.read(baseMap); - canvas.setBackgroundMap(map); - } catch (IOException e) { - e.printStackTrace(); + MapPlot canvas = new MapPlot(config, p); + try { + BufferedImage map = ImageIO.read(baseMap); + canvas.setBackgroundMap(map); + } catch (IOException e) { + e.printStackTrace(); + } + + canvas.drawTitle(); + canvas.drawAxes(); + + ColorRamp ramp = ColorRamp.createHue(-1, 1); + Rectangle2D bounds = gp.getBounds2D(); + canvas.plot( + func, + ramp, + new AxisRange(Math.toRadians(133), Math.toRadians(bounds.getMaxX())), + new AxisRange(Math.toRadians(bounds.getMinY()), Math.toRadians(bounds.getMaxY()))); + + DiscreteDataSet outline = new DiscreteDataSet(""); + double[] coords = new double[6]; + for (PathIterator pi = gp.getPathIterator(null); !pi.isDone(); pi.next()) { + pi.currentSegment(coords); + outline.add(Math.toRadians(coords[0]), Math.toRadians(coords[1])); + } + + canvas.plot(outline); + outline.setSymbol(new Circle().setFill(true).setSize(4)); + outline.setColor(Color.GREEN); + canvas.plot(outline); + + Annotations a = new Annotations(); + a.addAnnotation( + ImmutableAnnotation.builder().text("Perth").build(), Math.toRadians(115.82), Math.toRadians(-31.97)); + a.addAnnotation( + ImmutableAnnotation.builder() + .text("Singapore") + .color(Color.WHITE) + .font(new Font("Times New Roman", Font.BOLD, 24)) + .build(), + Math.toRadians(103.9), + Math.toRadians(1.20)); + + canvas.drawLatLonGrid(Math.toRadians(10), Math.toRadians(10), true); + canvas.addAnnotations(a); + + return canvas.getImage(); } - canvas.drawTitle(); - canvas.drawAxes(); + public static void main(String[] args) { + URL input = MapPlotDemo.class.getResource("earth_day_4096.jpg"); + try { + File f = File.createTempFile("resource-", ".jpg"); + f.deleteOnExit(); - ColorRamp ramp = ColorRamp.createHue(-1, 1); - Rectangle2D bounds = gp.getBounds2D(); - canvas.plot( - func, - ramp, - new AxisRange(Math.toRadians(133), Math.toRadians(bounds.getMaxX())), - new AxisRange(Math.toRadians(bounds.getMinY()), Math.toRadians(bounds.getMaxY()))); + assert input != null; + FileUtils.copyURLToFile(input, f); - DiscreteDataSet outline = new DiscreteDataSet(""); - double[] coords = new double[6]; - for (PathIterator pi = gp.getPathIterator(null); !pi.isDone(); pi.next()) { - pi.currentSegment(coords); - outline.add(Math.toRadians(coords[0]), Math.toRadians(coords[1])); + BufferedImage image; + image = makeOrthographic(f); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + image = makeRectangular(f); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("doc/images/mapPlotDemo.png", image); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } } - - canvas.plot(outline); - outline.setSymbol(new Circle().setFill(true).setSize(4)); - outline.setColor(Color.GREEN); - canvas.plot(outline); - - Annotations a = new Annotations(); - a.addAnnotation( - ImmutableAnnotation.builder().text("Perth").build(), - Math.toRadians(115.82), - Math.toRadians(-31.97)); - a.addAnnotation( - ImmutableAnnotation.builder() - .text("Singapore") - .color(Color.WHITE) - .font(new Font("Times New Roman", Font.BOLD, 24)) - .build(), - Math.toRadians(103.9), - Math.toRadians(1.20)); - - canvas.drawLatLonGrid(Math.toRadians(10), Math.toRadians(10), true); - canvas.addAnnotations(a); - - return canvas.getImage(); - } - - public static void main(String[] args) { - URL input = MapPlotDemo.class.getResource("earth_day_4096.jpg"); - try { - File f = File.createTempFile("resource-", ".jpg"); - f.deleteOnExit(); - - assert input != null; - FileUtils.copyURLToFile(input, f); - - BufferedImage image; - image = makeOrthographic(f); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); - image = makeRectangular(f); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); - // PlotCanvas.writeImage("doc/images/mapPlotDemo.png", image); - } catch (IOException e) { - logger.error(e.getLocalizedMessage(), e); - } - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/MultiPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/MultiPlotDemo.java index de5846e..79b7165 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/MultiPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/MultiPlotDemo.java @@ -30,31 +30,28 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class MultiPlotDemo { - public static BufferedImage makePlot() { - BufferedImage topImage = LinePlotDemo.makePlot(); - BufferedImage bottomImage = ActivityPlotDemo.makePlot(); + public static BufferedImage makePlot() { + BufferedImage topImage = LinePlotDemo.makePlot(); + BufferedImage bottomImage = ActivityPlotDemo.makePlot(); - int width = 600; - int height = 800; - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + int width = 600; + int height = 800; + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); - Graphics2D g = image.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(topImage, 0, 0, width, height / 2, 0, 0, topImage.getWidth(), topImage.getHeight(), - null); - g.drawImage(bottomImage, 0, height / 2, width, height, 0, 0, bottomImage.getWidth(), - bottomImage.getHeight(), null); - g.dispose(); + Graphics2D g = image.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.drawImage(topImage, 0, 0, width, height / 2, 0, 0, topImage.getWidth(), topImage.getHeight(), null); + g.drawImage( + bottomImage, 0, height / 2, width, height, 0, 0, bottomImage.getWidth(), bottomImage.getHeight(), null); + g.dispose(); - return image; - } - - public static void main(String[] args) { - BufferedImage image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); - // PlotCanvas.writeImage("multiPlotDemo.png", image); - } + return image; + } + public static void main(String[] args) { + BufferedImage image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("multiPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/PolarPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/PolarPlotDemo.java index 514ebdf..2eea61b 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/PolarPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/PolarPlotDemo.java @@ -42,60 +42,58 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class PolarPlotDemo { - private static DiscreteDataSet createCosX(Interval range) { - DiscreteDataSet ds = new DiscreteDataSet("cos"); - for (int i = 0; i < 100; i++) { - double theta = range.getInf() + range.getSize() * i / 99.; - ds.add(1 + Math.cos(theta), theta); + private static DiscreteDataSet createCosX(Interval range) { + DiscreteDataSet ds = new DiscreteDataSet("cos"); + for (int i = 0; i < 100; i++) { + double theta = range.getInf() + range.getSize() * i / 99.; + ds.add(1 + Math.cos(theta), theta); + } + return ds; } - return ds; - } - public static BufferedImage makePlot() { - AxisR axisR = new AxisR(0, 1.5, "R"); - NavigableMap tickLabels = new TreeMap<>(); - for (double r = axisR.getRange().getMin(); r < axisR.getRange().getMax(); r += .5) - tickLabels.put(r, String.format("%.1f", r)); - axisR.setTickLabels(tickLabels, 2); + public static BufferedImage makePlot() { + AxisR axisR = new AxisR(0, 1.5, "R"); + NavigableMap tickLabels = new TreeMap<>(); + for (double r = axisR.getRange().getMin(); r < axisR.getRange().getMax(); r += .5) + tickLabels.put(r, String.format("%.1f", r)); + axisR.setTickLabels(tickLabels, 2); - AxisTheta axisTheta = new AxisTheta(0, 2 * Math.PI, "θ", StringFunctions.toDegrees("%.0f")); - tickLabels = new TreeMap<>(); - for (double deg = 0; deg <= 360; deg += 60) - tickLabels.put(Math.toRadians(deg), String.format("%.0f", deg)); - axisTheta.setTickLabels(tickLabels, 6); + AxisTheta axisTheta = new AxisTheta(0, 2 * Math.PI, "θ", StringFunctions.toDegrees("%.0f")); + tickLabels = new TreeMap<>(); + for (double deg = 0; deg <= 360; deg += 60) tickLabels.put(Math.toRadians(deg), String.format("%.0f", deg)); + axisTheta.setTickLabels(tickLabels, 6); - DiscreteDataSet dds = createCosX(new Interval(-Math.PI, Math.PI)); - dds.setSymbol(new Square().setFill(true).setSize(4).setRotate(45)); + DiscreteDataSet dds = createCosX(new Interval(-Math.PI, Math.PI)); + dds.setSymbol(new Square().setFill(true).setSize(4).setRotate(45)); - double rotate = -Math.PI / 2; + double rotate = -Math.PI / 2; - PlotConfig config = - ImmutablePlotConfig.builder() - .width(800) - .height(600) - .title("Polar Plot Demo, rotate " + Math.toDegrees(rotate)) - .build(); - PolarPlot canvas = new PolarPlot(config, axisR, axisTheta); - canvas.setRotate(rotate); - canvas.drawTitle(); - canvas.drawGrid(); - canvas.drawAxes(); - canvas.plot(dds); + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Polar Plot Demo, rotate " + Math.toDegrees(rotate)) + .build(); + PolarPlot canvas = new PolarPlot(config, axisR, axisTheta); + canvas.setRotate(rotate); + canvas.drawTitle(); + canvas.drawGrid(); + canvas.drawAxes(); + canvas.plot(dds); - Point2D point = canvas.dataToPixel(axisR, 0.75, Math.toRadians(300)); - canvas.addAnnotation( - ImmutableAnnotation.builder().text("0.75, 300").color(Color.RED).build(), - point.getX(), - point.getY(), - rotate); + Point2D point = canvas.dataToPixel(axisR, 0.75, Math.toRadians(300)); + canvas.addAnnotation( + ImmutableAnnotation.builder().text("0.75, 300").color(Color.RED).build(), + point.getX(), + point.getY(), + rotate); - return canvas.getImage(); - } + return canvas.getImage(); + } - public static void main(String[] args) { - BufferedImage image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); -// PlotCanvas.writeImage("doc/images/polarPlotDemo.png", image); - } + public static void main(String[] args) { + BufferedImage image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("doc/images/polarPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/SandPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/SandPlotDemo.java index 5e95ef5..a257b8f 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/SandPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/SandPlotDemo.java @@ -40,54 +40,59 @@ import terrasaur.utils.saaPlotLib.util.PlotUtils; public class SandPlotDemo { - private static DiscreteDataSet createRandomDataSet(AxisRange range, String name) { - DiscreteDataSet ds = new DiscreteDataSet(name); - for (int i = 0; i < 100; i++) { - double x = range.getMin() + range.getLength() * i / 99.; - double y = new Random().nextDouble(); - ds.add(x, y); - } - return ds; - } - - public static BufferedImage makePlot() { - PlotConfig config = - ImmutablePlotConfig.builder().width(800).height(600).title("Sand Plot Example").build(); - - config = ImmutablePlotConfig.builder().from(config) - .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) - .legendFont(new Font("Helvetica", Font.BOLD, 24)).gridColor(Color.LIGHT_GRAY) - .legendColor(true).legendOutline(true).build(); - - int nCurves = 4; - AxisX xLowerAxis = new AxisX(0, 10, "X Axis"); - AxisY yLeftAxis = new AxisY(0, nCurves, "Y Axis"); - - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - - for (int i = 0; i < 4; i++) { - DiscreteDataSet thisData = - createRandomDataSet(xLowerAxis.getRange(), String.format("data set %d", i)); - thisData.setPlotType(PLOTTYPE.SAND); - float h = ((float) i) / nCurves; - thisData.setColor(Color.getHSBColor(h, 1.0f, 1.0f)); - canvas.plot(thisData); - canvas.addToLegend(thisData.getLegendEntry()); + private static DiscreteDataSet createRandomDataSet(AxisRange range, String name) { + DiscreteDataSet ds = new DiscreteDataSet(name); + for (int i = 0; i < 100; i++) { + double x = range.getMin() + range.getLength() * i / 99.; + double y = new Random().nextDouble(); + ds.add(x, y); + } + return ds; } - canvas.drawAxes(); - canvas.drawLegend(); - canvas.drawGrid(); + public static BufferedImage makePlot() { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Sand Plot Example") + .build(); - return canvas.getImage(); - } + config = ImmutablePlotConfig.builder() + .from(config) + .legendPosition(new Point2D.Double(config.leftMargin() + 50, config.topMargin() + 50)) + .legendFont(new Font("Helvetica", Font.BOLD, 24)) + .gridColor(Color.LIGHT_GRAY) + .legendColor(true) + .legendOutline(true) + .build(); - public static void main(String[] args) { - BufferedImage image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); -// PlotCanvas.writeImage("doc/images/sandPlotDemo.png", image); - } + int nCurves = 4; + AxisX xLowerAxis = new AxisX(0, 10, "X Axis"); + AxisY yLeftAxis = new AxisY(0, nCurves, "Y Axis"); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + + for (int i = 0; i < 4; i++) { + DiscreteDataSet thisData = createRandomDataSet(xLowerAxis.getRange(), String.format("data set %d", i)); + thisData.setPlotType(PLOTTYPE.SAND); + float h = ((float) i) / nCurves; + thisData.setColor(Color.getHSBColor(h, 1.0f, 1.0f)); + canvas.plot(thisData); + canvas.addToLegend(thisData.getLegendEntry()); + } + + canvas.drawAxes(); + canvas.drawLegend(); + canvas.drawGrid(); + + return canvas.getImage(); + } + + public static void main(String[] args) { + BufferedImage image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + // PlotCanvas.writeImage("doc/images/sandPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/demo/ScatterPlotDemo.java b/src/main/java/terrasaur/utils/saaPlotLib/demo/ScatterPlotDemo.java index 3733a7b..89ad24a 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/demo/ScatterPlotDemo.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/demo/ScatterPlotDemo.java @@ -41,36 +41,39 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class ScatterPlotDemo { - public static BufferedImage makePlot() { - DiscreteDataSet scatter = new DiscreteDataSet("data"); - int npts = 1000; - Random r = new Random(); - ColorRamp ramp = ColorRamp.createLinear(0, npts); - scatter.setColorRamp(ramp); - for (int i = 0; i < npts; i++) scatter.add(r.nextGaussian(), 3 * r.nextGaussian(), 0, i); + public static BufferedImage makePlot() { + DiscreteDataSet scatter = new DiscreteDataSet("data"); + int npts = 1000; + Random r = new Random(); + ColorRamp ramp = ColorRamp.createLinear(0, npts); + scatter.setColorRamp(ramp); + for (int i = 0; i < npts; i++) scatter.add(r.nextGaussian(), 3 * r.nextGaussian(), 0, i); - scatter.setSymbol(new Square().setFill(true).setRotate(45)); + scatter.setSymbol(new Square().setFill(true).setRotate(45)); - AxisX xAxis = new AxisX(10, -10, "X Axis"); - AxisY yAxis = new AxisY(-10, 10, "Y Axis"); - PlotConfig config = ImmutablePlotConfig.builder().build(); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - canvas.setAxes(xAxis, yAxis); - canvas.drawAxes(); - canvas.plot(scatter); + AxisX xAxis = new AxisX(10, -10, "X Axis"); + AxisY yAxis = new AxisY(-10, 10, "Y Axis"); + PlotConfig config = ImmutablePlotConfig.builder().build(); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + canvas.setAxes(xAxis, yAxis); + canvas.drawAxes(); + canvas.plot(scatter); - ColorBar colorBar = ImmutableColorBar.builder() - .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)).ramp(ramp).numTicks(5) - .tickFunction(StringFunctions.fixedFormat("%.0f")).build(); - canvas.drawColorBar(colorBar); + ColorBar colorBar = ImmutableColorBar.builder() + .rect(new Rectangle(config.leftMargin(), 40, config.width(), 10)) + .ramp(ramp) + .numTicks(5) + .tickFunction(StringFunctions.fixedFormat("%.0f")) + .build(); + canvas.drawColorBar(colorBar); - return canvas.getImage(); - } + return canvas.getImage(); + } - public static void main(String[] args) { - BufferedImage image = makePlot(); - PlotUtils.addCreationDate(image); - PlotCanvas.showJFrame(image); - PlotCanvas.writeImage("doc/images/scatterPlotDemo.png", image); - } + public static void main(String[] args) { + BufferedImage image = makePlot(); + PlotUtils.addCreationDate(image); + PlotCanvas.showJFrame(image); + PlotCanvas.writeImage("doc/images/scatterPlotDemo.png", image); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/util/Keyword.java b/src/main/java/terrasaur/utils/saaPlotLib/util/Keyword.java index 1359dce..722354b 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/util/Keyword.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/util/Keyword.java @@ -23,7 +23,9 @@ package terrasaur.utils.saaPlotLib.util; public enum Keyword { - - ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM - + ALIGN_LEFT, + ALIGN_RIGHT, + ALIGN_CENTER, + ALIGN_TOP, + ALIGN_BOTTOM } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/util/LegendEntry.java b/src/main/java/terrasaur/utils/saaPlotLib/util/LegendEntry.java index 6a27340..7a5a2cd 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/util/LegendEntry.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/util/LegendEntry.java @@ -31,22 +31,22 @@ import terrasaur.utils.saaPlotLib.canvas.symbol.Symbol; @Value.Immutable public abstract class LegendEntry implements Comparable { - @Value.Default - public Color color() { - return Color.BLACK; - } + @Value.Default + public Color color() { + return Color.BLACK; + } - @Value.Default - public String name() { - return ""; - } + @Value.Default + public String name() { + return ""; + } - public abstract Optional stroke(); + public abstract Optional stroke(); - public abstract Optional symbol(); + public abstract Optional symbol(); - @Override - public int compareTo(LegendEntry o) { - return name().compareTo(o.name()); - } + @Override + public int compareTo(LegendEntry o) { + return name().compareTo(o.name()); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/util/PlotUtils.java b/src/main/java/terrasaur/utils/saaPlotLib/util/PlotUtils.java index 3e17451..153783a 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/util/PlotUtils.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/util/PlotUtils.java @@ -33,190 +33,182 @@ import terrasaur.utils.saaPlotLib.canvas.PlotCanvas; public class PlotUtils { - /** - * Return a value greater than number with the desired number of significant digits. For example, - * if the number is 8803.364: - * - *

    - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    digitsroundUpper
    010000
    19000
    28900
    38810
    - * - * @param number number to round - * @param digits number of significant digits - * @return number rounded up - */ - public static double getRoundCeiling(double number, int digits) { - int sign = (int) Math.signum(number); - if (sign == 0) return sign; + /** + * Return a value greater than number with the desired number of significant digits. For example, + * if the number is 8803.364: + * + *

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    digitsroundUpper
    010000
    19000
    28900
    38810
    + * + * @param number number to round + * @param digits number of significant digits + * @return number rounded up + */ + public static double getRoundCeiling(double number, int digits) { + int sign = (int) Math.signum(number); + if (sign == 0) return sign; - number = Math.abs(number); - if (sign > 0) { - int exponent = (int) Math.floor(Math.log10(number) - digits + 1); - int mantissa = (int) Math.ceil(Math.pow(10, Math.log10(number) - exponent)); - return sign * mantissa * Math.pow(10, exponent); - } else { - return sign * getRoundFloor(number, digits); + number = Math.abs(number); + if (sign > 0) { + int exponent = (int) Math.floor(Math.log10(number) - digits + 1); + int mantissa = (int) Math.ceil(Math.pow(10, Math.log10(number) - exponent)); + return sign * mantissa * Math.pow(10, exponent); + } else { + return sign * getRoundFloor(number, digits); + } } - } - /** - * Return a value less than number with the desired number of significant digits. For example, if - * the number is 8803.364: - * - *

    - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    digitsroundUpper
    00
    18000
    28800
    38800
    - * - * @param number number to round - * @param digits number of significant digits - * @return number rounded down - */ - public static double getRoundFloor(double number, int digits) { - int sign = (int) Math.signum(number); - if (sign == 0) return sign; + /** + * Return a value less than number with the desired number of significant digits. For example, if + * the number is 8803.364: + * + *

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    digitsroundUpper
    00
    18000
    28800
    38800
    + * + * @param number number to round + * @param digits number of significant digits + * @return number rounded down + */ + public static double getRoundFloor(double number, int digits) { + int sign = (int) Math.signum(number); + if (sign == 0) return sign; - number = Math.abs(number); - if (sign > 0) { - int exponent = (int) Math.floor(Math.log10(number) - digits + 1); - int mantissa = (int) Math.floor(Math.pow(10, Math.log10(number) - exponent)); - return sign * mantissa * Math.pow(10, exponent); - } else { - return sign * getRoundCeiling(number, digits); + number = Math.abs(number); + if (sign > 0) { + int exponent = (int) Math.floor(Math.log10(number) - digits + 1); + int mantissa = (int) Math.floor(Math.pow(10, Math.log10(number) - exponent)); + return sign * mantissa * Math.pow(10, exponent); + } else { + return sign * getRoundCeiling(number, digits); + } } - } - /** - * Write the current time/date at the lower left. For example, "Created Mon Dec 28 16:07:04 EST - * 2020" - * - * @param image image to annotate - */ - public static void addCreationDate(BufferedImage image) { - addCreationDate(image, "Created %s"); - } - - /** - * Write the current time/date at the lower left. For example, "Created Mon Dec 28 16:07:04 EST - * 2020" - * - * @param image image to annotate - * @param format %s can be used to include {@link Date#toString()} - */ - public static void addCreationDate(BufferedImage image, String format) { - Graphics2D g = image.createGraphics(); - PlotCanvas.configureHintsForSubpixelQuality(g); - g.setColor(Color.BLACK); - - String date = String.format(format, new Date()); - Point2D.Double coords = - StringUtils.stringCoordinates( - g, - date, - image.getWidth() - 10, - image.getHeight() - 10, - Keyword.ALIGN_BOTTOM, - Keyword.ALIGN_RIGHT); - g.drawString(date, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); - } - - /** - * Write a string in the lower left. - * - * @param image image to annotate - * @param text to write - */ - public static void addAnnotation(BufferedImage image, String text) { - Graphics2D g = image.createGraphics(); - PlotCanvas.configureHintsForSubpixelQuality(g); - g.setColor(Color.BLACK); - - if (text.trim().isEmpty()) return; - - String[] lines = text.split("\n"); - - Keyword vert = Keyword.ALIGN_BOTTOM; - Keyword horiz = Keyword.ALIGN_RIGHT; - - double x = image.getWidth() - 10; - double y = image.getHeight() - 10; - - for (int i = lines.length - 1; i >= 0; i--) { - String line = lines[i]; - Point2D.Double coords = StringUtils.stringCoordinates(g, line, x, y, vert, horiz); - g.drawString(line, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); - y -= g.getFontMetrics().getHeight(); + /** + * Write the current time/date at the lower left. For example, "Created Mon Dec 28 16:07:04 EST + * 2020" + * + * @param image image to annotate + */ + public static void addCreationDate(BufferedImage image) { + addCreationDate(image, "Created %s"); } - } - /** - * @param src source image - * @param ratio less than 1 to create a smaller image, greater than 1 for a larger image - * @return scaled image - */ - public static BufferedImage createThumbnail(BufferedImage src, double ratio) { - int w = (int) (src.getWidth() * ratio); - int h = (int) (src.getHeight() * ratio); - BufferedImage dst = new BufferedImage(w, h, src.getType()); - dst.createGraphics().drawImage(src.getScaledInstance(w, h, Image.SCALE_SMOOTH), 0, 0, null); - return dst; - } + /** + * Write the current time/date at the lower left. For example, "Created Mon Dec 28 16:07:04 EST + * 2020" + * + * @param image image to annotate + * @param format %s can be used to include {@link Date#toString()} + */ + public static void addCreationDate(BufferedImage image, String format) { + Graphics2D g = image.createGraphics(); + PlotCanvas.configureHintsForSubpixelQuality(g); + g.setColor(Color.BLACK); - public static void main(String[] args) { - Random r = new Random(); - double base = r.nextDouble(); - int exponent = r.nextInt(6); - for (int i = 0; i < 10; i++) { - double number = base * Math.pow(10, exponent); - System.out.printf( - "%2d %f %f %f ", i, number, getRoundFloor(number, i), getRoundCeiling(number, i)); - System.out.printf( - "%f %f %f\n", -number, getRoundFloor(-number, i), getRoundCeiling(-number, i)); + String date = String.format(format, new Date()); + Point2D.Double coords = StringUtils.stringCoordinates( + g, date, image.getWidth() - 10, image.getHeight() - 10, Keyword.ALIGN_BOTTOM, Keyword.ALIGN_RIGHT); + g.drawString(date, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); + } + + /** + * Write a string in the lower left. + * + * @param image image to annotate + * @param text to write + */ + public static void addAnnotation(BufferedImage image, String text) { + Graphics2D g = image.createGraphics(); + PlotCanvas.configureHintsForSubpixelQuality(g); + g.setColor(Color.BLACK); + + if (text.trim().isEmpty()) return; + + String[] lines = text.split("\n"); + + Keyword vert = Keyword.ALIGN_BOTTOM; + Keyword horiz = Keyword.ALIGN_RIGHT; + + double x = image.getWidth() - 10; + double y = image.getHeight() - 10; + + for (int i = lines.length - 1; i >= 0; i--) { + String line = lines[i]; + Point2D.Double coords = StringUtils.stringCoordinates(g, line, x, y, vert, horiz); + g.drawString(line, (int) (coords.getX() + 0.5), (int) (coords.getY() + 0.5)); + y -= g.getFontMetrics().getHeight(); + } + } + + /** + * @param src source image + * @param ratio less than 1 to create a smaller image, greater than 1 for a larger image + * @return scaled image + */ + public static BufferedImage createThumbnail(BufferedImage src, double ratio) { + int w = (int) (src.getWidth() * ratio); + int h = (int) (src.getHeight() * ratio); + BufferedImage dst = new BufferedImage(w, h, src.getType()); + dst.createGraphics().drawImage(src.getScaledInstance(w, h, Image.SCALE_SMOOTH), 0, 0, null); + return dst; + } + + public static void main(String[] args) { + Random r = new Random(); + double base = r.nextDouble(); + int exponent = r.nextInt(6); + for (int i = 0; i < 10; i++) { + double number = base * Math.pow(10, exponent); + System.out.printf("%2d %f %f %f ", i, number, getRoundFloor(number, i), getRoundCeiling(number, i)); + System.out.printf("%f %f %f\n", -number, getRoundFloor(-number, i), getRoundCeiling(-number, i)); + } } - } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/util/StringFunctions.java b/src/main/java/terrasaur/utils/saaPlotLib/util/StringFunctions.java index 3c0ca88..a9235f2 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/util/StringFunctions.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/util/StringFunctions.java @@ -26,45 +26,45 @@ import java.util.function.Function; public class StringFunctions { - public static Function fixedFormat(String format) { - return (t) -> String.format(format, t); - } + public static Function fixedFormat(String format) { + return (t) -> String.format(format, t); + } - public static Function scale(String format, double scale) { - return (t) -> String.format(format, t * scale); - } + public static Function scale(String format, double scale) { + return (t) -> String.format(format, t * scale); + } - public static Function toDegrees(String format) { - return (t) -> String.format(format, Math.toDegrees(t)); - } + public static Function toDegrees(String format) { + return (t) -> String.format(format, Math.toDegrees(t)); + } - public static Function toDegreesLat(String format) { - return (t) -> String.format(format + (t > 0 ? "N" : "S"), Math.abs(Math.toDegrees(t))); - } + public static Function toDegreesLat(String format) { + return (t) -> String.format(format + (t > 0 ? "N" : "S"), Math.abs(Math.toDegrees(t))); + } - public static Function toDegreesLon(String format) { - return (t) -> String.format(format + (t > 0 ? "E" : "W"), Math.abs(Math.toDegrees(t))); - } + public static Function toDegreesLon(String format) { + return (t) -> String.format(format + (t > 0 ? "E" : "W"), Math.abs(Math.toDegrees(t))); + } - public static Function toDegreesELon(String format) { - return t -> { - t = Math.toDegrees(t); - if (t < 0) t += 360; - if (t >= 360) t -= 360; - return String.format(format + "E", t); - }; - } + public static Function toDegreesELon(String format) { + return t -> { + t = Math.toDegrees(t); + if (t < 0) t += 360; + if (t >= 360) t -= 360; + return String.format(format + "E", t); + }; + } - public static Function toDegreesWLon(String format) { - return t -> { - t = -Math.toDegrees(t); - if (t < 0) t += 360; - if (t > 360) t -= 360; - return String.format(format + "W", t); - }; - } + public static Function toDegreesWLon(String format) { + return t -> { + t = -Math.toDegrees(t); + if (t < 0) t += 360; + if (t > 360) t -= 360; + return String.format(format + "W", t); + }; + } - public static Function toRadians(String format) { - return (t) -> String.format(format, Math.toRadians(t)); - } + public static Function toRadians(String format) { + return (t) -> String.format(format, Math.toRadians(t)); + } } diff --git a/src/main/java/terrasaur/utils/saaPlotLib/util/StringUtils.java b/src/main/java/terrasaur/utils/saaPlotLib/util/StringUtils.java index 954aedf..5e6d6cc 100644 --- a/src/main/java/terrasaur/utils/saaPlotLib/util/StringUtils.java +++ b/src/main/java/terrasaur/utils/saaPlotLib/util/StringUtils.java @@ -31,80 +31,80 @@ import java.awt.geom.Rectangle2D; public class StringUtils { - /** - * @param g Graphics2D - * @param text text to draw - * @param x x pixel coordinate - * @param y y pixel coordinate - * @return the X, Y coordinates to draw the string with its desired alignment at (x, y) - */ - public static Point2D.Double stringCoordinates( - Graphics2D g, String text, double x, double y, Keyword vert, Keyword horiz) { + /** + * @param g Graphics2D + * @param text text to draw + * @param x x pixel coordinate + * @param y y pixel coordinate + * @return the X, Y coordinates to draw the string with its desired alignment at (x, y) + */ + public static Point2D.Double stringCoordinates( + Graphics2D g, String text, double x, double y, Keyword vert, Keyword horiz) { - Rectangle2D r = boundingBox(g, text); - double width = r.getWidth(); - double height = r.getHeight(); + Rectangle2D r = boundingBox(g, text); + double width = r.getWidth(); + double height = r.getHeight(); - double widthOffset = 0; - double heightOffset = 0; + double widthOffset = 0; + double heightOffset = 0; - switch (vert) { - case ALIGN_CENTER: - heightOffset = height / 2; - break; - case ALIGN_TOP: - heightOffset = height; - break; - case ALIGN_BOTTOM: - default: + switch (vert) { + case ALIGN_CENTER: + heightOffset = height / 2; + break; + case ALIGN_TOP: + heightOffset = height; + break; + case ALIGN_BOTTOM: + default: + } + + switch (horiz) { + case ALIGN_CENTER: + widthOffset = -width / 2; + break; + case ALIGN_RIGHT: + widthOffset = -width; + break; + case ALIGN_LEFT: + default: + } + + return new Point2D.Double(x + widthOffset, y + heightOffset); } - switch (horiz) { - case ALIGN_CENTER: - widthOffset = -width / 2; - break; - case ALIGN_RIGHT: - widthOffset = -width; - break; - case ALIGN_LEFT: - default: + public static double stringWidth(Graphics2D g, String text) { + return boundingBox(g, text).getWidth(); } - return new Point2D.Double(x + widthOffset, y + heightOffset); - } + public static double stringHeight(Graphics2D g, String text) { + return boundingBox(g, text).getHeight(); + } - public static double stringWidth(Graphics2D g, String text) { - return boundingBox(g, text).getWidth(); - } + public static Rectangle2D boundingBox(Graphics2D g, String text) { + FontRenderContext frc = g.getFontRenderContext(); + GlyphVector v = g.getFont().createGlyphVector(frc, text); + return v.getVisualBounds(); + } - public static double stringHeight(Graphics2D g, String text) { - return boundingBox(g, text).getHeight(); - } + /** + * @param g Graphics2D + * @param text text to draw + * @param x x pixel value at lower left + * @param y y pixel value at lower left + */ + public static void drawOutlinedString(Graphics2D g, String text, double x, double y) { + g.drawString(text, (int) (x + 0.5), (int) (y + 0.5)); + Rectangle2D r = boundingBox(g, text); - public static Rectangle2D boundingBox(Graphics2D g, String text) { - FontRenderContext frc = g.getFontRenderContext(); - GlyphVector v = g.getFont().createGlyphVector(frc, text); - return v.getVisualBounds(); - } + // logger.debug("x, y, box w, h: {} {} {} {}", x, y, r.getWidth(), r.getHeight()); - /** - * @param g Graphics2D - * @param text text to draw - * @param x x pixel value at lower left - * @param y y pixel value at lower left - */ - public static void drawOutlinedString(Graphics2D g, String text, double x, double y) { - g.drawString(text, (int) (x + 0.5), (int) (y + 0.5)); - Rectangle2D r = boundingBox(g, text); - - // logger.debug("x, y, box w, h: {} {} {} {}", x, y, r.getWidth(), r.getHeight()); - - Path2D.Double path = new Path2D.Double(); - path.moveTo(x, y); - path.lineTo(x + r.getWidth(), y); - path.lineTo(x + r.getWidth(), y - r.getHeight()); - path.lineTo(x, y - r.getHeight()); - path.closePath(); - g.draw(path); - } + Path2D.Double path = new Path2D.Double(); + path.moveTo(x, y); + path.lineTo(x + r.getWidth(), y); + path.lineTo(x + r.getWidth(), y - r.getHeight()); + path.lineTo(x, y - r.getHeight()); + path.closePath(); + g.draw(path); + } } diff --git a/src/main/java/terrasaur/utils/spice/SpiceBundle.java b/src/main/java/terrasaur/utils/spice/SpiceBundle.java index 57ecfce..b4ab15c 100644 --- a/src/main/java/terrasaur/utils/spice/SpiceBundle.java +++ b/src/main/java/terrasaur/utils/spice/SpiceBundle.java @@ -49,8 +49,8 @@ import picante.spice.SpiceEnvironmentBuilder; import picante.spice.adapters.SpiceEphemerisID; import picante.spice.adapters.SpiceFrameID; import picante.spice.kernel.KernelInstantiationException; -import picante.spice.kernelpool.UnwritableKernelPool; import picante.spice.kernelpool.KernelPool; +import picante.spice.kernelpool.UnwritableKernelPool; import picante.spice.provided.EphemerisNames; import picante.spice.provided.FrameNames; import picante.time.TimeConversion; @@ -58,645 +58,650 @@ import picante.time.TimeConversion; /** * Container class with a {@link SpiceEnvironment}, {@link AberratedEphemerisProvider}, and * {@link UnwritableKernelPool}. - * + * * @author nairah1 * */ public class SpiceBundle { - private final static Logger logger = LogManager.getLogger(SpiceBundle.class); + private static final Logger logger = LogManager.getLogger(SpiceBundle.class); - private SpiceEnvironment spiceEnv; - private AberratedEphemerisProvider abProvider; - private UnwritableKernelPool kernelPool; - private TimeConversion timeConversion; + private SpiceEnvironment spiceEnv; + private AberratedEphemerisProvider abProvider; + private UnwritableKernelPool kernelPool; + private TimeConversion timeConversion; - private List loadedKernels; - private Map objectNameBindings; - private Map objectIDBindings; - private Map frameNameBindings; - private Map frameIDBindings; - private Map bodyFixedFrames; - - public static final class Builder { - private List kernels; - private KernelPool kernelPool; - private List additionalEphemerisSources; - private List additionalFrameSources; - private Map additionalFrameCenters; - private boolean lockable; - private SpiceEnvironmentBuilder builder; + private List loadedKernels; private Map objectNameBindings; private Map objectIDBindings; private Map frameNameBindings; private Map frameIDBindings; + private Map bodyFixedFrames; - /** - * Add these kernels to the kernel list - * - * @param kernels - * @return - */ - public Builder addKernelList(List kernels) { - this.kernels.addAll(kernels); - return this; - } + public static final class Builder { + private List kernels; + private KernelPool kernelPool; + private List additionalEphemerisSources; + private List additionalFrameSources; + private Map additionalFrameCenters; + private boolean lockable; + private SpiceEnvironmentBuilder builder; + private Map objectNameBindings; + private Map objectIDBindings; + private Map frameNameBindings; + private Map frameIDBindings; - /** - * Add this kernel pool to the existing kernel pool - * - * @param kernelPool - * @return - */ - public Builder addKernelPool(KernelPool kernelPool) { - this.kernelPool.load(kernelPool); - return this; - } - - /** - * Add the kernels and kernel pool variables in this list of metakernels to the existing kernel - * list and kernel pool - * - * @param metakernels - * @return - */ - public Builder addMetakernels(List metakernels) { - List kernelPaths = new ArrayList<>(); - KernelPool kernelPool = new KernelPool(); - - for (String mk : metakernels) { - MetakernelReader mkReader = new MetakernelReader(mk); - if (mkReader.isGood()) { - kernelPaths.addAll(mkReader.getKernelsToLoad()); - kernelPool.load(mkReader.getKernelPool()); - if (mkReader.hasWarnings()) { - for (String s : mkReader.getWarnLog()) - logger.warn(s.trim()); - } - } else { - for (String s : mkReader.getErrLog()) - logger.warn(s.trim()); - logger.warn("Did not load {}", mk); + /** + * Add these kernels to the kernel list + * + * @param kernels + * @return + */ + public Builder addKernelList(List kernels) { + this.kernels.addAll(kernels); + return this; } - } - addKernelList(kernelPaths); - addKernelPool(kernelPool); - return this; - } - - /** - * Add these ephemerisSources to the {@link EphemerisAndFrameProvider} after the SPICE kernels - * have been loaded. - * - * @param funcs - * @return - */ - public Builder addEphemerisSources(List funcs) { - additionalEphemerisSources.addAll(funcs); - return this; - } - - /** - * Add these frameSources to the {@link EphemerisAndFrameProvider} after the SPICE kernels have - * been loaded. - * - * @param funcs - * @return - */ - public Builder addFrameSources(List funcs) { - additionalFrameSources.addAll(funcs); - return this; - } - - /** - * Add these FrameID -> EphemerisID mappings. - * - * @param map - * @return - */ - public Builder addFrameCenters(Map map) { - additionalFrameCenters.putAll(map); - return this; - } - - /** - * If true, use a {@link LockableEphemerisProvider} rather than the default - * {@link ReferenceEphemerisProvider}. - * - * @param lockable - * @return - */ - public Builder setLockable(boolean lockable) { - this.lockable = lockable; - return this; - } - - public Builder setSpiceEnvironmentBuilder(SpiceEnvironmentBuilder builder) { - this.builder = builder; - return this; - } - - public Builder() { - this.kernels = new ArrayList<>(); - this.kernelPool = new KernelPool(); - this.additionalEphemerisSources = new ArrayList<>(); - this.additionalFrameSources = new ArrayList<>(); - this.additionalFrameCenters = new LinkedHashMap<>(); - lockable = false; - builder = new SpiceEnvironmentBuilder(); - objectNameBindings = new HashMap<>(); - objectIDBindings = new HashMap<>(); - frameNameBindings = new HashMap<>(); - frameIDBindings = new HashMap<>(); - } - - public SpiceBundle build() { - try { - for (File kernel : kernels) { - try { - builder.load(kernel.getCanonicalPath(), kernel); - } catch (KernelInstantiationException e) { - logger.warn(e.getLocalizedMessage()); - logger.warn(String.format("Using forgiving loader for unsupported kernel format: %s.", - kernel.getPath())); - builder.forgivingLoad(kernel.getCanonicalPath(), kernel); - } + /** + * Add this kernel pool to the existing kernel pool + * + * @param kernelPool + * @return + */ + public Builder addKernelPool(KernelPool kernelPool) { + this.kernelPool.load(kernelPool); + return this; } - } catch (KernelInstantiationException | IOException e) { - logger.warn(e.getLocalizedMessage(), e); - } - // now bind name/code pairs to ephemeris IDs - KernelPool mkPool = new KernelPool(kernelPool); - SpiceEnvironment env = builder.build(); - UnwritableKernelPool envPool = env.getPool(); - KernelPool fullPool = new KernelPool(); - fullPool.load(envPool); - fullPool.load(mkPool); + /** + * Add the kernels and kernel pool variables in this list of metakernels to the existing kernel + * list and kernel pool + * + * @param metakernels + * @return + */ + public Builder addMetakernels(List metakernels) { + List kernelPaths = new ArrayList<>(); + KernelPool kernelPool = new KernelPool(); - // map of SPICE codes to ephemeris objects - Map boundIds = new HashMap<>(); - - // add built in objects - EphemerisNames builtInIds = new EphemerisNames(); - for (Integer key : builtInIds.getStandardBindings().keySet()) { - EphemerisID value = builtInIds.getStandardBindings().get(key); - boundIds.put(key, value); - objectNameBindings.put(value.getName().toUpperCase(), value); - objectIDBindings.put(key, value); - } - - // add built in frames - FrameNames builtInFrames = new FrameNames(); - for (Integer key : builtInFrames.getStandardBindings().keySet()) { - FrameID value = builtInFrames.getStandardBindings().get(key); - frameNameBindings.put(value.getName().toUpperCase(), value); - frameIDBindings.put(key, value); - } - - for (PositionVectorFunction f : env.getEphemerisSources()) { - // initialize with known objects in SPK files - EphemerisID id = f.getTargetID(); - if (id instanceof SpiceEphemerisID) { - SpiceEphemerisID spiceID = (SpiceEphemerisID) id; - boundIds.put(spiceID.getIDCode(), spiceID); - } - } - - if (envPool.hasKeyword("NAIF_BODY_NAME") && envPool.hasKeyword("NAIF_BODY_CODE")) { - List names = envPool.getStrings("NAIF_BODY_NAME"); - List codes = envPool.getIntegers("NAIF_BODY_CODE"); - - if (names.size() != codes.size()) - logger.warn(String.format( - "NAIF_BODY_CODE has %d entries while NAIF_BODY_NAME has %d entries. Will not bind any of these ids.", - codes.size(), names.size())); - else { - for (int i = 0; i < codes.size(); i++) { - int code = codes.get(i); - String name = names.get(i); - SpiceEphemerisID spiceID = new SpiceEphemerisID(code, name); - objectNameBindings.put(name.toUpperCase(), spiceID); - objectIDBindings.put(code, spiceID); - // builder.bindEphemerisID(name, spiceID); - new SpkCodeBinder(code, name, spiceID).configure(builder); - logger.debug("From text kernel: binding name {} to code {}", name, code); - boundIds.put(code, spiceID); - } - } - } - - // bind SPICE ids defined in metakernels - if (mkPool.hasKeyword("NAIF_BODY_NAME") && mkPool.hasKeyword("NAIF_BODY_CODE")) { - List names = mkPool.getStrings("NAIF_BODY_NAME"); - List codes = mkPool.getIntegers("NAIF_BODY_CODE"); - - if (names.size() != codes.size()) - logger.warn(String.format( - "NAIF_BODY_CODE has %d entries while NAIF_BODY_NAME has %d entries. Will not bind any of these ids.", - codes.size(), names.size())); - else { - for (int i = 0; i < codes.size(); i++) { - int code = codes.get(i); - String name = names.get(i); - SpiceEphemerisID spiceID = new SpiceEphemerisID(code, name); - objectNameBindings.put(name.toUpperCase(), spiceID); - objectIDBindings.put(code, spiceID); - // builder.bindEphemerisID(name, spiceID); - new SpkCodeBinder(code, name, spiceID).configure(builder); - logger.debug("From metakernel: binding name {} to code {}", name, code); - boundIds.put(code, spiceID); - } - } - } - - // now populate frame lookup maps - Set keywords = fullPool.getKeywords(); - for (String keyword : keywords) { - if (keyword.startsWith("FRAME_") && keyword.endsWith("_NAME")) { - String[] parts = keyword.split("_"); - int id = Integer.parseInt(parts[1]); - - String spiceName = fullPool.getStrings(keyword).get(0); - String frameKeyword = String.format("FRAME_%s", spiceName); - if (!fullPool.hasKeyword(frameKeyword)) { - logger.warn("Kernel pool does not contain keyword " + frameKeyword); - continue; - } - Integer spiceFrameCode = fullPool.getDoubles(frameKeyword).get(0).intValue(); - - if (spiceFrameCode != id) { - logger.warn( - String.format("Expected keyword %s to be %d, found %d instead. Skipping this one.", - frameKeyword, id, spiceFrameCode)); - continue; - } - - FrameID frameID = new SpiceFrameID(spiceFrameCode); - frameNameBindings.put(spiceName, frameID); - frameIDBindings.put(spiceFrameCode, frameID); - builder.bindFrameID(spiceName, frameID); - - // The FRAME_XXX_CENTER can have a numeric or string value - String centerKey = String.format("FRAME_%d_CENTER", spiceFrameCode); - - EphemerisID spiceID = null; - String centerCodeString = ""; - if (fullPool.isStringValued(centerKey)) { - String centerCode = fullPool.getStrings(centerKey).get(0); - spiceID = objectNameBindings.get(centerCode); - centerCodeString = centerCode; - } else { - int centerCode = 0; - if (fullPool.isDoubleValued(centerKey)) { - centerCode = fullPool.getDoubles(centerKey).get(0).intValue(); - } else if (fullPool.isIntegerValued(centerKey)) { - centerCode = fullPool.getIntegers(centerKey).get(0); + for (String mk : metakernels) { + MetakernelReader mkReader = new MetakernelReader(mk); + if (mkReader.isGood()) { + kernelPaths.addAll(mkReader.getKernelsToLoad()); + kernelPool.load(mkReader.getKernelPool()); + if (mkReader.hasWarnings()) { + for (String s : mkReader.getWarnLog()) logger.warn(s.trim()); + } + } else { + for (String s : mkReader.getErrLog()) logger.warn(s.trim()); + logger.warn("Did not load {}", mk); + } } - spiceID = boundIds.get(centerCode); - centerCodeString = String.format("%d", centerCode); - } - if (spiceID == null) - logger.warn(String.format("Unknown ephemeris object specified by FRAME_%d_CENTER (%s).", - spiceFrameCode, centerCodeString)); - else { - additionalFrameCenters.put(frameID, spiceID); - logger.debug("Binding SPICE frame {} to frame code {}", frameID.getName(), - spiceFrameCode); - } + addKernelList(kernelPaths); + addKernelPool(kernelPool); + return this; } - } - // populate body fixed map - keywords = fullPool.getKeywords(); - Map bodyFixedFrames = new HashMap<>(); - for (EphemerisID body : objectNameBindings.values()) { - FrameID iauFrame = - frameNameBindings.get(String.format("IAU_%s", body.getName().toUpperCase())); - if (iauFrame != null) - bodyFixedFrames.put(body, iauFrame); - } - for (String keyword : keywords) { - if (keyword.startsWith("OBJECT_") && keyword.endsWith("_FRAME")) { - String[] parts = keyword.split("_"); - EphemerisID thisBody = null; - try { - Integer idCode = Integer.parseInt(parts[1]); - thisBody = objectIDBindings.get(idCode); - } catch (NumberFormatException e) { - thisBody = objectNameBindings.get(parts[1].toUpperCase()); - } - if (thisBody != null) { - FrameID thisFrame = null; - if (fullPool.isIntegerValued(keyword)) - thisFrame = frameIDBindings.get(fullPool.getIntegers(keyword).get(0)); - else - thisFrame = frameNameBindings.get(fullPool.getStrings(keyword).get(0).toUpperCase()); - if (thisFrame != null) - bodyFixedFrames.put(thisBody, thisFrame); - } + /** + * Add these ephemerisSources to the {@link EphemerisAndFrameProvider} after the SPICE kernels + * have been loaded. + * + * @param funcs + * @return + */ + public Builder addEphemerisSources(List funcs) { + additionalEphemerisSources.addAll(funcs); + return this; } - } - env = builder.build(); - List envEphSources = new ArrayList<>(env.getEphemerisSources()); - envEphSources.addAll(additionalEphemerisSources); - List envFrameSources = new ArrayList<>(env.getFrameSources()); - envFrameSources.addAll(additionalFrameSources); + /** + * Add these frameSources to the {@link EphemerisAndFrameProvider} after the SPICE kernels have + * been loaded. + * + * @param funcs + * @return + */ + public Builder addFrameSources(List funcs) { + additionalFrameSources.addAll(funcs); + return this; + } - EphemerisAndFrameProvider provider = - lockable ? new LockableEphemerisProvider(envEphSources, envFrameSources) - : new ReferenceEphemerisProvider(envEphSources, envFrameSources); + /** + * Add these FrameID -> EphemerisID mappings. + * + * @param map + * @return + */ + public Builder addFrameCenters(Map map) { + additionalFrameCenters.putAll(map); + return this; + } - SpiceBundle bundle = new SpiceBundle(); - bundle.spiceEnv = env; - Map frameCenters = new LinkedHashMap<>(env.getFrameCenterMap()); - frameCenters.putAll(additionalFrameCenters); + /** + * If true, use a {@link LockableEphemerisProvider} rather than the default + * {@link ReferenceEphemerisProvider}. + * + * @param lockable + * @return + */ + public Builder setLockable(boolean lockable) { + this.lockable = lockable; + return this; + } - bundle.abProvider = AberratedEphemerisProvider.createSingleIteration(provider, frameCenters); - KernelPool kp = new KernelPool(); - kp.load(env.getPool()); - kp.load(kernelPool); - bundle.kernelPool = new UnwritableKernelPool(kp); - bundle.loadedKernels = Collections.unmodifiableList(kernels); - bundle.objectNameBindings = Collections.unmodifiableMap(objectNameBindings); - bundle.objectIDBindings = Collections.unmodifiableMap(objectIDBindings); - bundle.frameNameBindings = Collections.unmodifiableMap(frameNameBindings); - bundle.frameIDBindings = Collections.unmodifiableMap(frameIDBindings); - bundle.bodyFixedFrames = Collections.unmodifiableMap(bodyFixedFrames); + public Builder setSpiceEnvironmentBuilder(SpiceEnvironmentBuilder builder) { + this.builder = builder; + return this; + } - // initialize TimeSystems with this kernel pool - bundle.timeConversion = new TimeConversion(env.getLSK()); + public Builder() { + this.kernels = new ArrayList<>(); + this.kernelPool = new KernelPool(); + this.additionalEphemerisSources = new ArrayList<>(); + this.additionalFrameSources = new ArrayList<>(); + this.additionalFrameCenters = new LinkedHashMap<>(); + lockable = false; + builder = new SpiceEnvironmentBuilder(); + objectNameBindings = new HashMap<>(); + objectIDBindings = new HashMap<>(); + frameNameBindings = new HashMap<>(); + frameIDBindings = new HashMap<>(); + } - return bundle; + public SpiceBundle build() { + try { + for (File kernel : kernels) { + try { + builder.load(kernel.getCanonicalPath(), kernel); + } catch (KernelInstantiationException e) { + logger.warn(e.getLocalizedMessage()); + logger.warn(String.format( + "Using forgiving loader for unsupported kernel format: %s.", kernel.getPath())); + builder.forgivingLoad(kernel.getCanonicalPath(), kernel); + } + } + } catch (KernelInstantiationException | IOException e) { + logger.warn(e.getLocalizedMessage(), e); + } + + // now bind name/code pairs to ephemeris IDs + KernelPool mkPool = new KernelPool(kernelPool); + SpiceEnvironment env = builder.build(); + UnwritableKernelPool envPool = env.getPool(); + KernelPool fullPool = new KernelPool(); + fullPool.load(envPool); + fullPool.load(mkPool); + + // map of SPICE codes to ephemeris objects + Map boundIds = new HashMap<>(); + + // add built in objects + EphemerisNames builtInIds = new EphemerisNames(); + for (Integer key : builtInIds.getStandardBindings().keySet()) { + EphemerisID value = builtInIds.getStandardBindings().get(key); + boundIds.put(key, value); + objectNameBindings.put(value.getName().toUpperCase(), value); + objectIDBindings.put(key, value); + } + + // add built in frames + FrameNames builtInFrames = new FrameNames(); + for (Integer key : builtInFrames.getStandardBindings().keySet()) { + FrameID value = builtInFrames.getStandardBindings().get(key); + frameNameBindings.put(value.getName().toUpperCase(), value); + frameIDBindings.put(key, value); + } + + for (PositionVectorFunction f : env.getEphemerisSources()) { + // initialize with known objects in SPK files + EphemerisID id = f.getTargetID(); + if (id instanceof SpiceEphemerisID) { + SpiceEphemerisID spiceID = (SpiceEphemerisID) id; + boundIds.put(spiceID.getIDCode(), spiceID); + } + } + + if (envPool.hasKeyword("NAIF_BODY_NAME") && envPool.hasKeyword("NAIF_BODY_CODE")) { + List names = envPool.getStrings("NAIF_BODY_NAME"); + List codes = envPool.getIntegers("NAIF_BODY_CODE"); + + if (names.size() != codes.size()) + logger.warn(String.format( + "NAIF_BODY_CODE has %d entries while NAIF_BODY_NAME has %d entries. Will not bind any of these ids.", + codes.size(), names.size())); + else { + for (int i = 0; i < codes.size(); i++) { + int code = codes.get(i); + String name = names.get(i); + SpiceEphemerisID spiceID = new SpiceEphemerisID(code, name); + objectNameBindings.put(name.toUpperCase(), spiceID); + objectIDBindings.put(code, spiceID); + // builder.bindEphemerisID(name, spiceID); + new SpkCodeBinder(code, name, spiceID).configure(builder); + logger.debug("From text kernel: binding name {} to code {}", name, code); + boundIds.put(code, spiceID); + } + } + } + + // bind SPICE ids defined in metakernels + if (mkPool.hasKeyword("NAIF_BODY_NAME") && mkPool.hasKeyword("NAIF_BODY_CODE")) { + List names = mkPool.getStrings("NAIF_BODY_NAME"); + List codes = mkPool.getIntegers("NAIF_BODY_CODE"); + + if (names.size() != codes.size()) + logger.warn(String.format( + "NAIF_BODY_CODE has %d entries while NAIF_BODY_NAME has %d entries. Will not bind any of these ids.", + codes.size(), names.size())); + else { + for (int i = 0; i < codes.size(); i++) { + int code = codes.get(i); + String name = names.get(i); + SpiceEphemerisID spiceID = new SpiceEphemerisID(code, name); + objectNameBindings.put(name.toUpperCase(), spiceID); + objectIDBindings.put(code, spiceID); + // builder.bindEphemerisID(name, spiceID); + new SpkCodeBinder(code, name, spiceID).configure(builder); + logger.debug("From metakernel: binding name {} to code {}", name, code); + boundIds.put(code, spiceID); + } + } + } + + // now populate frame lookup maps + Set keywords = fullPool.getKeywords(); + for (String keyword : keywords) { + if (keyword.startsWith("FRAME_") && keyword.endsWith("_NAME")) { + String[] parts = keyword.split("_"); + int id = Integer.parseInt(parts[1]); + + String spiceName = fullPool.getStrings(keyword).get(0); + String frameKeyword = String.format("FRAME_%s", spiceName); + if (!fullPool.hasKeyword(frameKeyword)) { + logger.warn("Kernel pool does not contain keyword " + frameKeyword); + continue; + } + Integer spiceFrameCode = + fullPool.getDoubles(frameKeyword).get(0).intValue(); + + if (spiceFrameCode != id) { + logger.warn(String.format( + "Expected keyword %s to be %d, found %d instead. Skipping this one.", + frameKeyword, id, spiceFrameCode)); + continue; + } + + FrameID frameID = new SpiceFrameID(spiceFrameCode); + frameNameBindings.put(spiceName, frameID); + frameIDBindings.put(spiceFrameCode, frameID); + builder.bindFrameID(spiceName, frameID); + + // The FRAME_XXX_CENTER can have a numeric or string value + String centerKey = String.format("FRAME_%d_CENTER", spiceFrameCode); + + EphemerisID spiceID = null; + String centerCodeString = ""; + if (fullPool.isStringValued(centerKey)) { + String centerCode = fullPool.getStrings(centerKey).get(0); + spiceID = objectNameBindings.get(centerCode); + centerCodeString = centerCode; + } else { + int centerCode = 0; + if (fullPool.isDoubleValued(centerKey)) { + centerCode = fullPool.getDoubles(centerKey).get(0).intValue(); + } else if (fullPool.isIntegerValued(centerKey)) { + centerCode = fullPool.getIntegers(centerKey).get(0); + } + spiceID = boundIds.get(centerCode); + centerCodeString = String.format("%d", centerCode); + } + + if (spiceID == null) + logger.warn(String.format( + "Unknown ephemeris object specified by FRAME_%d_CENTER (%s).", + spiceFrameCode, centerCodeString)); + else { + additionalFrameCenters.put(frameID, spiceID); + logger.debug("Binding SPICE frame {} to frame code {}", frameID.getName(), spiceFrameCode); + } + } + } + + // populate body fixed map + keywords = fullPool.getKeywords(); + Map bodyFixedFrames = new HashMap<>(); + for (EphemerisID body : objectNameBindings.values()) { + FrameID iauFrame = frameNameBindings.get( + String.format("IAU_%s", body.getName().toUpperCase())); + if (iauFrame != null) bodyFixedFrames.put(body, iauFrame); + } + for (String keyword : keywords) { + if (keyword.startsWith("OBJECT_") && keyword.endsWith("_FRAME")) { + String[] parts = keyword.split("_"); + EphemerisID thisBody = null; + try { + Integer idCode = Integer.parseInt(parts[1]); + thisBody = objectIDBindings.get(idCode); + } catch (NumberFormatException e) { + thisBody = objectNameBindings.get(parts[1].toUpperCase()); + } + if (thisBody != null) { + FrameID thisFrame = null; + if (fullPool.isIntegerValued(keyword)) + thisFrame = frameIDBindings.get( + fullPool.getIntegers(keyword).get(0)); + else + thisFrame = frameNameBindings.get( + fullPool.getStrings(keyword).get(0).toUpperCase()); + if (thisFrame != null) bodyFixedFrames.put(thisBody, thisFrame); + } + } + } + + env = builder.build(); + List envEphSources = new ArrayList<>(env.getEphemerisSources()); + envEphSources.addAll(additionalEphemerisSources); + List envFrameSources = new ArrayList<>(env.getFrameSources()); + envFrameSources.addAll(additionalFrameSources); + + EphemerisAndFrameProvider provider = lockable + ? new LockableEphemerisProvider(envEphSources, envFrameSources) + : new ReferenceEphemerisProvider(envEphSources, envFrameSources); + + SpiceBundle bundle = new SpiceBundle(); + bundle.spiceEnv = env; + Map frameCenters = new LinkedHashMap<>(env.getFrameCenterMap()); + frameCenters.putAll(additionalFrameCenters); + + bundle.abProvider = AberratedEphemerisProvider.createSingleIteration(provider, frameCenters); + KernelPool kp = new KernelPool(); + kp.load(env.getPool()); + kp.load(kernelPool); + bundle.kernelPool = new UnwritableKernelPool(kp); + bundle.loadedKernels = Collections.unmodifiableList(kernels); + bundle.objectNameBindings = Collections.unmodifiableMap(objectNameBindings); + bundle.objectIDBindings = Collections.unmodifiableMap(objectIDBindings); + bundle.frameNameBindings = Collections.unmodifiableMap(frameNameBindings); + bundle.frameIDBindings = Collections.unmodifiableMap(frameIDBindings); + bundle.bodyFixedFrames = Collections.unmodifiableMap(bodyFixedFrames); + + // initialize TimeSystems with this kernel pool + bundle.timeConversion = new TimeConversion(env.getLSK()); + + return bundle; + } } - } - private SpiceBundle() {} + private SpiceBundle() {} - public SpiceEnvironment getSpiceEnv() { - return spiceEnv; - } + public SpiceEnvironment getSpiceEnv() { + return spiceEnv; + } - public AberratedEphemerisProvider getAbProvider() { - return abProvider; - } + public AberratedEphemerisProvider getAbProvider() { + return abProvider; + } - /** - * Returns the kernel pool. This includes key/value pairs supplied in metakernels to the builder, - * while {@link SpiceEnvironment#getPool()} does not. - * - * @return - */ - public UnwritableKernelPool getKernelPool() { - return kernelPool; - } + /** + * Returns the kernel pool. This includes key/value pairs supplied in metakernels to the builder, + * while {@link SpiceEnvironment#getPool()} does not. + * + * @return + */ + public UnwritableKernelPool getKernelPool() { + return kernelPool; + } - /** - * Return a TimeConversion object that can be used to convert between the UTC, TDB, TDT, TAI, and - * GPS time systems. - * - * @return - */ - public TimeConversion getTimeConversion() { - return timeConversion; - } + /** + * Return a TimeConversion object that can be used to convert between the UTC, TDB, TDT, TAI, and + * GPS time systems. + * + * @return + */ + public TimeConversion getTimeConversion() { + return timeConversion; + } - /** - * Return the last file in the list of loaded kernels matching supplied expression. - * - * @param regex Regular expression, used in a {@link Pattern} to match filenames in list of loaded - * kernels. - * - * @return file matching regex, or null if not found - */ - public File findKernel(String regex) { - Pattern p = Pattern.compile(regex); - return getKernels().stream().filter(f -> p.matcher(f.getPath()).matches()) - .reduce((first, second) -> second).orElse(null); - } + /** + * Return the last file in the list of loaded kernels matching supplied expression. + * + * @param regex Regular expression, used in a {@link Pattern} to match filenames in list of loaded + * kernels. + * + * @return file matching regex, or null if not found + */ + public File findKernel(String regex) { + Pattern p = Pattern.compile(regex); + return getKernels().stream() + .filter(f -> p.matcher(f.getPath()).matches()) + .reduce((first, second) -> second) + .orElse(null); + } - /** - * Return a list of kernels in the order they were loaded. - * - * @return - */ - public List getKernels() { - return loadedKernels; - } + /** + * Return a list of kernels in the order they were loaded. + * + * @return + */ + public List getKernels() { + return loadedKernels; + } - /** - * Split path on {@link File#separator}, return a list of strings each shorter than wrap that when - * concatenated return the original path. Intended for use when writing a metakernel; split paths - * longer than 80 characters into two strings. - * - * @param f - * @param wrap - * @return - */ - private List splitPath(File f, int wrap) { + /** + * Split path on {@link File#separator}, return a list of strings each shorter than wrap that when + * concatenated return the original path. Intended for use when writing a metakernel; split paths + * longer than 80 characters into two strings. + * + * @param f + * @param wrap + * @return + */ + private List splitPath(File f, int wrap) { - List list = new ArrayList<>(); - String[] parts = f.getAbsolutePath().split(File.separator); + List list = new ArrayList<>(); + String[] parts = f.getAbsolutePath().split(File.separator); - StringBuffer sb = new StringBuffer(parts[0]); - for (int i = 1; i < parts.length; i++) { - if (sb.toString().length() + parts[i].length() > wrap) { - sb.append(File.separator); + StringBuffer sb = new StringBuffer(parts[0]); + for (int i = 1; i < parts.length; i++) { + if (sb.toString().length() + parts[i].length() > wrap) { + sb.append(File.separator); + list.add(sb.toString()); + sb = new StringBuffer(parts[i]); + } else { + sb.append(String.format("%s%s", File.separator, parts[i])); + } + } list.add(sb.toString()); - sb = new StringBuffer(parts[i]); - } else { - sb.append(String.format("%s%s", File.separator, parts[i])); - } + + return list; } - list.add(sb.toString()); - return list; - } + /** + * Write a metakernel containing all kernels loaded in this bundle + * + * @param file + */ + public void writeSingleMetaKernel(File file, String comments) { + try (PrintWriter pw = new PrintWriter(file)) { + pw.println("KPL/MK"); + pw.println(comments); + pw.println("\\begindata\n"); + pw.println("KERNELS_TO_LOAD = ("); + for (File f : loadedKernels) { - /** - * Write a metakernel containing all kernels loaded in this bundle - * - * @param file - */ - public void writeSingleMetaKernel(File file, String comments) { - try (PrintWriter pw = new PrintWriter(file)) { - pw.println("KPL/MK"); - pw.println(comments); - pw.println("\\begindata\n"); - pw.println("KERNELS_TO_LOAD = ("); - for (File f : loadedKernels) { - - List parts = splitPath(f, 78); - if (parts.size() == 1) - pw.printf("'%s'\n", f.getAbsolutePath()); - else { - for (int i = 0; i < parts.size() - 1; i++) { - String part = parts.get(i); - pw.printf("'%s+'\n", part); - } - pw.printf("'%s'\n", parts.get(parts.size() - 1)); - } - } - pw.println(")\n"); - - // print out other _TO_LOAD variables. - for (String keyword : kernelPool.getKeywords()) { - if (keyword.endsWith("_TO_LOAD")) { - if (keyword.length() > 32) - logger.warn("Kernel variable {} has length {} (SPICE max is 32 characters)", keyword, - keyword.length()); - pw.printf("%s = (\n", keyword); - for (String value : kernelPool.getStrings(keyword)) { - File f = new File(value); - List parts = splitPath(f, 78); - if (parts.size() == 1) - pw.printf("'%s'\n", f.getAbsolutePath()); - else { - for (int i = 0; i < parts.size() - 1; i++) { - String part = parts.get(i); - pw.printf("'%s+'\n", part); - } - pw.printf("'%s'\n", parts.get(parts.size() - 1)); + List parts = splitPath(f, 78); + if (parts.size() == 1) pw.printf("'%s'\n", f.getAbsolutePath()); + else { + for (int i = 0; i < parts.size() - 1; i++) { + String part = parts.get(i); + pw.printf("'%s+'\n", part); + } + pw.printf("'%s'\n", parts.get(parts.size() - 1)); + } } - } - pw.println(")\n"); + pw.println(")\n"); + + // print out other _TO_LOAD variables. + for (String keyword : kernelPool.getKeywords()) { + if (keyword.endsWith("_TO_LOAD")) { + if (keyword.length() > 32) + logger.warn( + "Kernel variable {} has length {} (SPICE max is 32 characters)", + keyword, + keyword.length()); + pw.printf("%s = (\n", keyword); + for (String value : kernelPool.getStrings(keyword)) { + File f = new File(value); + List parts = splitPath(f, 78); + if (parts.size() == 1) pw.printf("'%s'\n", f.getAbsolutePath()); + else { + for (int i = 0; i < parts.size() - 1; i++) { + String part = parts.get(i); + pw.printf("'%s+'\n", part); + } + pw.printf("'%s'\n", parts.get(parts.size() - 1)); + } + } + pw.println(")\n"); + } + } + pw.println("\\begintext"); + } catch (FileNotFoundException e) { + logger.error(e.getLocalizedMessage(), e); } - } - pw.println("\\begintext"); - } catch (FileNotFoundException e) { - logger.error(e.getLocalizedMessage(), e); } - } - /** - * @param idCode - * @return the {@link EphemerisID} from the pool of known objects with the given id or null if - * there is no such object - */ - public EphemerisID getObject(int idCode) { - EphemerisID object = objectIDBindings.get(idCode); - if (object == null) { - try { - object = getAbProvider().getKnownObjects(new HashSet<>()).stream().filter( - id -> id instanceof SpiceEphemerisID && ((SpiceEphemerisID) id).getIDCode() == idCode) - .findFirst().get(); - } catch (NoSuchElementException e) { - logger.warn("No object with id {} has been defined", idCode); - } - } - return object; - } - - /** - * @param name - * @return the {@link EphemerisID} from the pool of known objects with the given name or null if - * there is no such object - */ - public EphemerisID getObject(String name) { - EphemerisID object = objectNameBindings.get(name.toUpperCase()); - if (object == null) { - try { - object = getAbProvider().getKnownObjects(new HashSet<>()).stream() - .filter(id -> name.equalsIgnoreCase(id.getName())).findFirst().get(); - } catch (NoSuchElementException e) { - try { - object = getObject(Integer.parseInt(name)); - } catch (NumberFormatException e1) { + /** + * @param idCode + * @return the {@link EphemerisID} from the pool of known objects with the given id or null if + * there is no such object + */ + public EphemerisID getObject(int idCode) { + EphemerisID object = objectIDBindings.get(idCode); + if (object == null) { + try { + object = getAbProvider().getKnownObjects(new HashSet<>()).stream() + .filter(id -> id instanceof SpiceEphemerisID && ((SpiceEphemerisID) id).getIDCode() == idCode) + .findFirst() + .get(); + } catch (NoSuchElementException e) { + logger.warn("No object with id {} has been defined", idCode); + } } - if (object == null) - logger.warn("No object {} has been defined", name); - } + return object; } - return object; - } - - /** - * @param name - * @return the {@link FrameID} from the pool of known frames with the given name - */ - public FrameID getFrame(String name) { - FrameID frame = frameNameBindings.get(name); - if (frame == null) { - try { - frame = getAbProvider().getKnownFrames(new HashSet<>()).stream() - .filter(id -> name.equalsIgnoreCase(id.getName())).findFirst().get(); - } catch (NoSuchElementException e) { - try { - frame = getFrame(Integer.parseInt(name)); - } catch (NumberFormatException e1) { + /** + * @param name + * @return the {@link EphemerisID} from the pool of known objects with the given name or null if + * there is no such object + */ + public EphemerisID getObject(String name) { + EphemerisID object = objectNameBindings.get(name.toUpperCase()); + if (object == null) { + try { + object = getAbProvider().getKnownObjects(new HashSet<>()).stream() + .filter(id -> name.equalsIgnoreCase(id.getName())) + .findFirst() + .get(); + } catch (NoSuchElementException e) { + try { + object = getObject(Integer.parseInt(name)); + } catch (NumberFormatException e1) { + } + if (object == null) logger.warn("No object {} has been defined", name); + } } - if (frame == null) - logger.warn("No frame {} has been defined", name); - } + + return object; } - return frame; - } - /** - * Return the body fixed frame associated with this body. Normally this is the IAU frame. A kernel - * may define a body fixed frame using the OBJECT_*_FRAME keyword. From the "frames" required - * reading: - * - *

    -   *    OBJECT_<name or spk_id>_FRAME =  '<frame name>'
    -   * or
    -   *    OBJECT_<name or spk_id>_FRAME =  <frame ID code>
    -   * 
    - * - * @param body - * @return - */ - public FrameID getBodyFixedFrame(EphemerisID body) { - return bodyFixedFrames.get(body); - } - - /** - * - * @param idCode - * @return the {@link FrameID} from the pool of known frames with the given id - */ - public FrameID getFrame(int idCode) { - FrameID frame = frameIDBindings.get(idCode); - if (frame == null) { - try { - frame = getAbProvider().getFrameProvider().getKnownFrames(new HashSet<>()).stream() - .filter(id -> id instanceof SpiceFrameID && ((SpiceFrameID) id).getIDCode() == idCode) - .collect(Collectors.toList()).get(0); - } catch (NoSuchElementException e) { - logger.warn("No frame with id has been defined", idCode); - } + /** + * @param name + * @return the {@link FrameID} from the pool of known frames with the given name + */ + public FrameID getFrame(String name) { + FrameID frame = frameNameBindings.get(name); + if (frame == null) { + try { + frame = getAbProvider().getKnownFrames(new HashSet<>()).stream() + .filter(id -> name.equalsIgnoreCase(id.getName())) + .findFirst() + .get(); + } catch (NoSuchElementException e) { + try { + frame = getFrame(Integer.parseInt(name)); + } catch (NumberFormatException e1) { + } + if (frame == null) logger.warn("No frame {} has been defined", name); + } + } + return frame; } - return frame; - } - /** - * Add objects known to the provider as well as objects bound by metakernels to the supplied - * buffer. - * - * @param buffer the buffer to receive the additional ephemeris ID codes - * - * @return a reference to buffer for convenience. - */ - public Set getKnownObjects(Set buffer) { - getAbProvider().getKnownObjects(buffer); - buffer.addAll(objectIDBindings.values()); - return buffer; - } + /** + * Return the body fixed frame associated with this body. Normally this is the IAU frame. A kernel + * may define a body fixed frame using the OBJECT_*_FRAME keyword. From the "frames" required + * reading: + * + *
    +     *    OBJECT_<name or spk_id>_FRAME =  '<frame name>'
    +     * or
    +     *    OBJECT_<name or spk_id>_FRAME =  <frame ID code>
    +     * 
    + * + * @param body + * @return + */ + public FrameID getBodyFixedFrame(EphemerisID body) { + return bodyFixedFrames.get(body); + } - /** - * Add frames known to the provider as well as the frames bound by the metakernel to the supplied - * buffer. - * - * @param buffer the buffer to receive the additional frame ID codes - * @return a reference to buffer for convenience - */ - public Set getKnownFrames(Set buffer) { - getAbProvider().getFrameProvider().getKnownFrames(buffer); - buffer.addAll(frameIDBindings.values()); - return buffer; - } + /** + * + * @param idCode + * @return the {@link FrameID} from the pool of known frames with the given id + */ + public FrameID getFrame(int idCode) { + FrameID frame = frameIDBindings.get(idCode); + if (frame == null) { + try { + frame = getAbProvider().getFrameProvider().getKnownFrames(new HashSet<>()).stream() + .filter(id -> id instanceof SpiceFrameID && ((SpiceFrameID) id).getIDCode() == idCode) + .collect(Collectors.toList()) + .get(0); + } catch (NoSuchElementException e) { + logger.warn("No frame with id has been defined", idCode); + } + } + return frame; + } + + /** + * Add objects known to the provider as well as objects bound by metakernels to the supplied + * buffer. + * + * @param buffer the buffer to receive the additional ephemeris ID codes + * + * @return a reference to buffer for convenience. + */ + public Set getKnownObjects(Set buffer) { + getAbProvider().getKnownObjects(buffer); + buffer.addAll(objectIDBindings.values()); + return buffer; + } + + /** + * Add frames known to the provider as well as the frames bound by the metakernel to the supplied + * buffer. + * + * @param buffer the buffer to receive the additional frame ID codes + * @return a reference to buffer for convenience + */ + public Set getKnownFrames(Set buffer) { + getAbProvider().getFrameProvider().getKnownFrames(buffer); + buffer.addAll(frameIDBindings.values()); + return buffer; + } } diff --git a/src/main/java/terrasaur/utils/spice/SpkCodeBinder.java b/src/main/java/terrasaur/utils/spice/SpkCodeBinder.java index 340f83a..1089d33 100644 --- a/src/main/java/terrasaur/utils/spice/SpkCodeBinder.java +++ b/src/main/java/terrasaur/utils/spice/SpkCodeBinder.java @@ -31,44 +31,42 @@ import picante.spice.SpiceEnvironmentBuilder; public class SpkCodeBinder implements Blueprint { - private final int spkIdCode; - private final String spkName; - private final EphemerisID id; + private final int spkIdCode; + private final String spkName; + private final EphemerisID id; - public SpkCodeBinder(int spkIdCode, String spkName, EphemerisID id) { - super(); - this.spkIdCode = spkIdCode; - this.spkName = spkName; - this.id = id; - } - - private static final ByteSource sourceString(String name, int code) { - StringBuilder linesBuilder = new StringBuilder(); - - linesBuilder.append("\\begindata \n NAIF_BODY_NAME += ( '"); - linesBuilder.append(name); - linesBuilder.append("' ) \n NAIF_BODY_CODE += ( "); - linesBuilder.append(code); - linesBuilder.append(" ) \n"); - - ByteSource result = - ByteSource.wrap(linesBuilder.toString().getBytes(Charsets.ISO_8859_1)); - return result; - } - - @Override - public SpiceEnvironmentBuilder configure(SpiceEnvironmentBuilder builder) { - - ByteSource sourceStr = sourceString(spkName, spkIdCode); - try { - builder.load(Long.valueOf(System.nanoTime()).toString(), sourceStr); - } catch (Exception e) { - Throwables.propagate(e); + public SpkCodeBinder(int spkIdCode, String spkName, EphemerisID id) { + super(); + this.spkIdCode = spkIdCode; + this.spkName = spkName; + this.id = id; } - builder.bindEphemerisID(spkName, id); + private static final ByteSource sourceString(String name, int code) { + StringBuilder linesBuilder = new StringBuilder(); - return builder; - } + linesBuilder.append("\\begindata \n NAIF_BODY_NAME += ( '"); + linesBuilder.append(name); + linesBuilder.append("' ) \n NAIF_BODY_CODE += ( "); + linesBuilder.append(code); + linesBuilder.append(" ) \n"); + ByteSource result = ByteSource.wrap(linesBuilder.toString().getBytes(Charsets.ISO_8859_1)); + return result; + } + + @Override + public SpiceEnvironmentBuilder configure(SpiceEnvironmentBuilder builder) { + + ByteSource sourceStr = sourceString(spkName, spkIdCode); + try { + builder.load(Long.valueOf(System.nanoTime()).toString(), sourceStr); + } catch (Exception e) { + Throwables.propagate(e); + } + + builder.bindEphemerisID(spkName, id); + + return builder; + } } diff --git a/src/main/java/terrasaur/utils/tessellation/AbrateTessellation.java b/src/main/java/terrasaur/utils/tessellation/AbrateTessellation.java index d4bf56c..5ffc396 100644 --- a/src/main/java/terrasaur/utils/tessellation/AbrateTessellation.java +++ b/src/main/java/terrasaur/utils/tessellation/AbrateTessellation.java @@ -43,587 +43,581 @@ import picante.math.vectorspace.VectorIJK; /** * See - * + * * https://www.researchgate.net/publication/230745267_Spiral_Tessellation_on_the_Sphere - * + * *

    * based on Bob Demajistre's code for SIMPLEX - * - * + * + * * @author nairah1 * */ public class AbrateTessellation implements SphericalTessellation { - private static Logger logger = LogManager.getLogger(AbrateTessellation.class); + private static Logger logger = LogManager.getLogger(AbrateTessellation.class); - /* employs naming conventions from the paper where possible */ - private long n; - private long m; + /* employs naming conventions from the paper where possible */ + private long n; + private long m; - /** - * number of non-polar tiles in the tessellation (which is total number of tiles - 2). Use - * {@link #getNumTiles()} to get the actual number of tiles in the tessellation. - * - * @return - */ - public long getM() { - return m; - } - - /** - * Number of spiral turns in the tessellation - * - * @return - */ - public long getN() { - return n; - } - - /** - * Create a set of equal area tiles approximately uniformly distributed about the unit sphere. The - * number of tiles will not necessarily equal to npts. Use {@link #getNumTiles()} to get the - * actual number of tiles in the tessellation. - * - * @param nTiles Number of tiles to use - */ - public AbrateTessellation(long nTiles) { - double tileArea = 4 * FastMath.PI / nTiles; - // eq 6 - n = FastMath.round(FastMath.PI / FastMath.sqrt(tileArea)); - // eq 7 - m = FastMath.round( - 4. * FastMath.PI * FastMath.sin(FastMath.sqrt(tileArea)) / FastMath.pow(tileArea, 1.5)); - } - - /** - * Create a set of equal area tiles approximately uniformly distributed about the unit sphere. - * - * @param n number of spiral turns - * @param m number of non-polar tiles (which is total # of tiles - 2) - */ - public AbrateTessellation(long n, long m) { - this.n = n; - this.m = m; - } - - /** - * Create a set of tiles with the same areas and positions as the supplied tessellation. - * - * @param other - */ - public AbrateTessellation(AbrateTessellation other) { - this(other.n, other.m); - } - - /** - * Return a map where the key is the tile index and the value being a set of tile indices from the - * other tessellation with centers contained within the key tile. - * - * @param other should be a tessellation with smaller tiles, but doesn't have to be. - * @return - */ - public Map> mapTiles(AbrateTessellation other) { - Map> mapTiles = new HashMap<>(); - - for (long otherTile = 0; otherTile < other.getNumTiles(); otherTile++) { - long thisTile = getTileIndex(other.getTileCenter(otherTile)); - Set tiles = mapTiles.get(thisTile); - if (tiles == null) { - tiles = new HashSet<>(); - mapTiles.put(thisTile, tiles); - } - tiles.add(otherTile); + /** + * number of non-polar tiles in the tessellation (which is total number of tiles - 2). Use + * {@link #getNumTiles()} to get the actual number of tiles in the tessellation. + * + * @return + */ + public long getM() { + return m; } - return mapTiles; - } - - /** - * returns a vector to the position along the spiral, based on equation 1 - * - * @param t allowed range is -PI/2 to PI/2 - * @return - */ - private UnwritableVectorIJK gamma(double t) { - if (t < -FastMath.PI / 2) { - return new UnwritableVectorIJK(0, 0, 1); - } - if (t > FastMath.PI / 2) { - return new UnwritableVectorIJK(0, 0, -1); + /** + * Number of spiral turns in the tessellation + * + * @return + */ + public long getN() { + return n; } - double x = FastMath.cos(t) * FastMath.cos(n * FastMath.PI + 2. * n * t); - double y = FastMath.cos(t) * FastMath.sin(n * FastMath.PI + 2. * n * t); - double z = -FastMath.sin(t); - UnwritableVectorIJK result = new UnwritableVectorIJK(x, y, z); - return result; - } - - /** - * returns a vector to the position along the spiral, based on equation 1 - * - * @param t allowed range is -PI/2 to PI/2 - * @return - */ - private LatitudinalVector gammaLV(double t) { - return CoordConverters.convertToLatitudinal(gamma(t)); - } - - /** - * returns the position along the spiral for a given tile (eqn 4) - * - * @param itile - * @return - */ - private double tfun(double itile) { - double result = - FastMath.acos(FastMath.cos(FastMath.PI / (2. * n)) * (1. - 2. * (itile - 1.) / m)) - - (n + 1.) * FastMath.PI / (2. * n); - return result; - } - - /** - * Total number of tiles covering the surface. This is {@link #getM()} + 2 (polar tiles). - * - * @return - */ - @Override - public long getNumTiles() { - return m + 2; - } - - /** - * Returns the XYZ position of the desired vertex for a given tile in the tessellation. If this is - * the north or south pole the zero vector is returned. This value is also returned if the vnum is - * less than 0 or greater than 3 - * - * @param tile - * @param vnum vertex number of the quadrilateral: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    0upper left (the northwest corner)
    1upper right
    2lower right (southeast corner)
    3lower left
    - * @return returns the XYZ position of the desired vertex for a given tile in the tessellation - */ - public UnwritableVectorIJK tileVertexIJK(long tile, int vnum) { - - if (tile == 0) { - return new UnwritableVectorIJK(0, 0, 1); - } - if (tile == m + 1) { - return new UnwritableVectorIJK(0, 0, -1); + /** + * Create a set of equal area tiles approximately uniformly distributed about the unit sphere. The + * number of tiles will not necessarily equal to npts. Use {@link #getNumTiles()} to get the + * actual number of tiles in the tessellation. + * + * @param nTiles Number of tiles to use + */ + public AbrateTessellation(long nTiles) { + double tileArea = 4 * FastMath.PI / nTiles; + // eq 6 + n = FastMath.round(FastMath.PI / FastMath.sqrt(tileArea)); + // eq 7 + m = FastMath.round(4. * FastMath.PI * FastMath.sin(FastMath.sqrt(tileArea)) / FastMath.pow(tileArea, 1.5)); } - UnwritableVectorIJK result = new UnwritableVectorIJK(0, 0, 0); - - if (vnum < 0 || vnum > 3) { - return result; + /** + * Create a set of equal area tiles approximately uniformly distributed about the unit sphere. + * + * @param n number of spiral turns + * @param m number of non-polar tiles (which is total # of tiles - 2) + */ + public AbrateTessellation(long n, long m) { + this.n = n; + this.m = m; } - long mytile = ((vnum == 1) || (vnum == 2)) ? tile + 1 : tile; - double tadd = 0.; - if ((vnum == 2) || (vnum == 3)) { - tadd = FastMath.PI / n; + /** + * Create a set of tiles with the same areas and positions as the supplied tessellation. + * + * @param other + */ + public AbrateTessellation(AbrateTessellation other) { + this(other.n, other.m); } - result = gamma(tfun(mytile) + tadd); + /** + * Return a map where the key is the tile index and the value being a set of tile indices from the + * other tessellation with centers contained within the key tile. + * + * @param other should be a tessellation with smaller tiles, but doesn't have to be. + * @return + */ + public Map> mapTiles(AbrateTessellation other) { + Map> mapTiles = new HashMap<>(); - return result; - } - - /** - * Returns the longitude, latitude pair (radians) for a given tile in the tessellation. If this is - * the north or south pole the zero vector is returned. This value is also returned if the vnum is - * less than 0 or greater than 3. - * - * @param tile - * @param vnum vertex number of the quadrilateral: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    0upper left (the northwest corner)
    1upper right
    2lower right (southeast corner)
    3lower left
    - * @return returns the longitude, latitude pair (radians) for a given tile in the tessellation - */ - public LatitudinalVector tileVertex(long tile, int vnum) { - return CoordConverters.convertToLatitudinal(tileVertexIJK(tile, vnum)); - } - - @Override - public long getTileIndex(LatitudinalVector lv) { - long k = (long) ((n * FastMath.PI - lv.getLongitude() - 2. * n * lv.getLatitude()) - / (2. * FastMath.PI) + 1e-12); - double tt = lv.getLongitude() / (2. * n) + (FastMath.PI / n) * k - FastMath.PI / 2.; - double id = (m - * (FastMath.cos(FastMath.PI / (2. * n)) - - FastMath.cos(tt + (n + 1.) * FastMath.PI / (2. * n))) - / (2. * FastMath.cos(FastMath.PI / (2. * n)))) + 1.; - long result = (long) id; - - return result; - } - - @Override - public LatitudinalVector getTileCenter(long i) { - return CoordConverters.convertToLatitudinal(getTileCenterIJK(i)); - } - - /** - * Get the tile center as an {@link UnwritableVectorIJK}. - * - * @param i - * @return - */ - public UnwritableVectorIJK getTileCenterIJK(long i) { - VectorIJK buffer = new VectorIJK(); - if (i == 0) { - buffer.setTo(0, 0, 1); - } else if (i == m + 1) { - buffer.setTo(0, 0, -1); - } else { - for (int vnum = 0; vnum < 4; vnum++) { - buffer.setTo(VectorIJK.add(buffer, tileVertexIJK(i, vnum))); - } - buffer.unitize(); - } - return buffer; - } - - /** - * return tile to the left of this tile i.e., adjoining side 3-0 - * - * @param tile - * @return - */ - public long leftTile(long tile) { - long result = tile - 1; - return result < 0 ? 0 : result; - } - - /** - * return tile to the right of this tile i.e., adjoining side 1-2 - * - * @param tile - * @return - */ - public long rightTile(long tile) { - long result = tile + 1; - return result > m + 1 ? m + 1 : result; - } - - /** - * return a sorted set of tiles above or northward of a given tile, i.e., adjoining side 0-1 - * - * @param tile - * @return - */ - public NavigableSet aboveTiles(long tile) { - NavigableSet result = new TreeSet(); - - double ti = tfun(tile); - double ti1 = tfun(tile + 1); - double pin = FastMath.PI / n; - double pi2 = FastMath.PI / 2.; - - if (ti1 < (-pi2 + pin)) { - return result; // nothing north of this - } - LatitudinalVector p2 = gammaLV(ti1 - pin); - long d1 = 0; - /* the small thing is to make handle precision problems */ - double small = pin * 0.05; // this is 0.05 of a tile extent in latitude - - long d2 = getTileIndex(new LatitudinalVector(1, p2.getLatitude() - small, p2.getLongitude())); - if (FastMath.abs(d2 - tile) <= 1) { - d2 = getTileIndex(new LatitudinalVector(1, p2.getLatitude() + small, p2.getLongitude())); - } - if (ti > (-pi2 + pin)) { - LatitudinalVector p1 = gammaLV(ti - pin); - d1 = getTileIndex(new LatitudinalVector(1, p1.getLatitude() - small, p1.getLongitude())); - if (FastMath.abs(d1 - tile) <= 1) { - d1 = getTileIndex(new LatitudinalVector(1, p1.getLatitude() + small, p1.getLongitude())); - } - } - - for (long ic = d1; ic <= d2; ic++) { - result.add(ic); - } - return result; - } - - /** - * return a sorted set of tiles below or southward of a given tile, i.e., adjoining side 2-3 - * - * @param tile - * @return - */ - public NavigableSet belowTiles(long tile) { - NavigableSet result = new TreeSet(); - - double ti = tfun(tile); - double ti1 = tfun(tile + 1); - double pin = FastMath.PI / n; - double pi2 = FastMath.PI / 2.; - - if (tile > m) { - return result; // null - } - if (ti > (pi2 - 2. * pin)) { - result.add(m + 1); - return result; - } - LatitudinalVector p3 = gammaLV(ti1 + pin); - long d4 = m + 1; - /* the small thing is to make handle precision problems */ - double small = pin * 0.05; // this is 0.05 of a tile extent in latitude - - long d3 = getTileIndex(p3); - if (FastMath.abs(d3 - tile) <= 1) { - d3 = getTileIndex(new LatitudinalVector(1, p3.getLatitude() - small, p3.getLongitude())); - } - - if (ti1 < (pi2 - 2. * pin)) { - LatitudinalVector p4 = gammaLV(ti + pin); - d4 = getTileIndex(p4); - if (FastMath.abs(d4 - tile) <= 1) { - d4 = getTileIndex(new LatitudinalVector(1, p4.getLatitude() - small, p4.getLongitude())); - } - - } else { - result.add(m + 1); - return result; - } - - for (long ic = d4; ic <= d3; ic++) { - result.add(ic); - } - - return result; - } - - /** - * add all matching tiles along the spiral to the left and right of tileInRegion - * - * @param matches existing set of matching tiles - * @param tileInRegion starting tile to test - * @param predicate function to test tile center - * @return - */ - private void addLeftAndRightTiles(Set matches, Long tileInRegion, - Predicate predicate) { - - if (!matches.contains(tileInRegion) && predicate.test(getTileCenterIJK(tileInRegion))) { - matches.add(tileInRegion); - } - - long tileIndex = rightTile(tileInRegion); - while (true) { - if (!matches.contains(tileIndex)) { - if (!predicate.test(getTileCenterIJK(tileIndex))) { - break; + for (long otherTile = 0; otherTile < other.getNumTiles(); otherTile++) { + long thisTile = getTileIndex(other.getTileCenter(otherTile)); + Set tiles = mapTiles.get(thisTile); + if (tiles == null) { + tiles = new HashSet<>(); + mapTiles.put(thisTile, tiles); + } + tiles.add(otherTile); } - matches.add(tileIndex); - } - tileIndex = rightTile(tileIndex); - // south pole - if (tileIndex == m + 1) { - matches.add(tileIndex); - break; - } + + return mapTiles; } - tileIndex = leftTile(tileInRegion); - while (true) { - if (!matches.contains(tileIndex)) { - if (!predicate.test(getTileCenterIJK(tileIndex))) { - break; + /** + * returns a vector to the position along the spiral, based on equation 1 + * + * @param t allowed range is -PI/2 to PI/2 + * @return + */ + private UnwritableVectorIJK gamma(double t) { + if (t < -FastMath.PI / 2) { + return new UnwritableVectorIJK(0, 0, 1); } - matches.add(tileIndex); - } - tileIndex = leftTile(tileIndex); - // north pole - if (tileIndex == 0) { - matches.add(tileIndex); - break; - } + if (t > FastMath.PI / 2) { + return new UnwritableVectorIJK(0, 0, -1); + } + + double x = FastMath.cos(t) * FastMath.cos(n * FastMath.PI + 2. * n * t); + double y = FastMath.cos(t) * FastMath.sin(n * FastMath.PI + 2. * n * t); + double z = -FastMath.sin(t); + UnwritableVectorIJK result = new UnwritableVectorIJK(x, y, z); + return result; } - } - - /** - * Check every tile in the tessellation against the predicate. - * - * @param predicate - * @return - */ - public Set matchingTilesIJK(Predicate predicate) { - Set matches = new HashSet<>(); - - for (long tile = 0; tile < getNumTiles(); tile++) { - if (predicate.test(getTileCenterIJK(tile))) { - matches.add(tile); - } - } - return matches; - } - - /** - * Get the {@link Set} of contiguous tiles which satisfy the predicate. - * - * @param tileInRegion starting tile to test - * @param predicate function to test tile center - * @return set of tiles that satisfy the supplied predicate in the contiguous region including - * tileInRegion - */ - public Set matchingTiles(Long tileInRegion, Predicate predicate) { - Set matches = new HashSet<>(); - - Set tilesInOriginalLine = new HashSet<>(); - addLeftAndRightTiles(tilesInOriginalLine, tileInRegion, predicate); - - Set tilesInLine = tilesInOriginalLine; - matches.addAll(tilesInLine); - - // Add all tiles above this line - while (true) { - if (tilesInLine.size() == 0) { - break; - } - - Set aboveTiles = new HashSet<>(); - for (Long tile : tilesInLine) { - aboveTiles.addAll(aboveTiles(tile)); - } - - tilesInLine = new HashSet<>(); - for (Long tile : aboveTiles) { - addLeftAndRightTiles(tilesInLine, tile, predicate); - } - - // if this is false, then no new tiles were added to matches - if (!matches.addAll(tilesInLine)) { - break; - } + /** + * returns a vector to the position along the spiral, based on equation 1 + * + * @param t allowed range is -PI/2 to PI/2 + * @return + */ + private LatitudinalVector gammaLV(double t) { + return CoordConverters.convertToLatitudinal(gamma(t)); } - tilesInLine = tilesInOriginalLine; - // Add all tiles below this line - while (true) { - if (tilesInLine.size() == 0) { - break; - } - - Set belowTiles = new HashSet<>(); - for (Long tile : tilesInLine) { - belowTiles.addAll(belowTiles(tile)); - } - - tilesInLine = new HashSet<>(); - for (Long tile : belowTiles) { - addLeftAndRightTiles(tilesInLine, tile, predicate); - } - - // if this is false, then no new tiles were added to matches - if (!matches.addAll(tilesInLine)) { - break; - } + /** + * returns the position along the spiral for a given tile (eqn 4) + * + * @param itile + * @return + */ + private double tfun(double itile) { + double result = FastMath.acos(FastMath.cos(FastMath.PI / (2. * n)) * (1. - 2. * (itile - 1.) / m)) + - (n + 1.) * FastMath.PI / (2. * n); + return result; } - return matches; - } - - /** - * Return the set of tiles whose centers are contained within outline. - * - * @param outline - * @return - */ - public Set tilesWithin(List outline) { - - if (outline.size() == 0) { - return new HashSet<>(); + /** + * Total number of tiles covering the surface. This is {@link #getM()} + 2 (polar tiles). + * + * @return + */ + @Override + public long getNumTiles() { + return m + 2; } - VectorIJK centerIJK = new VectorIJK(); - for (LatitudinalVector lv : outline) { - centerIJK.setTo(VectorIJK.add(centerIJK, CoordConverters.convert(lv))); + /** + * Returns the XYZ position of the desired vertex for a given tile in the tessellation. If this is + * the north or south pole the zero vector is returned. This value is also returned if the vnum is + * less than 0 or greater than 3 + * + * @param tile + * @param vnum vertex number of the quadrilateral: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    0upper left (the northwest corner)
    1upper right
    2lower right (southeast corner)
    3lower left
    + * @return returns the XYZ position of the desired vertex for a given tile in the tessellation + */ + public UnwritableVectorIJK tileVertexIJK(long tile, int vnum) { + + if (tile == 0) { + return new UnwritableVectorIJK(0, 0, 1); + } + if (tile == m + 1) { + return new UnwritableVectorIJK(0, 0, -1); + } + + UnwritableVectorIJK result = new UnwritableVectorIJK(0, 0, 0); + + if (vnum < 0 || vnum > 3) { + return result; + } + + long mytile = ((vnum == 1) || (vnum == 2)) ? tile + 1 : tile; + double tadd = 0.; + if ((vnum == 2) || (vnum == 3)) { + tadd = FastMath.PI / n; + } + + result = gamma(tfun(mytile) + tadd); + + return result; } - centerIJK.scale(1. / outline.size()); - LatitudinalVector center = CoordConverters.convertToLatitudinal(centerIJK); - Long centerTile = getTileIndex(center); - - StereographicProjection proj = new StereographicProjection(center); - LatitudinalVector first = outline.get(0); - Point2D xy0 = proj.forward(first); - Path2D.Double path = new Path2D.Double(); - path.moveTo(xy0.getX(), xy0.getY()); - for (int i = 1; i < outline.size(); i++) { - Point2D xy = proj.forward(outline.get(i)); - path.lineTo(xy.getX(), xy.getY()); + /** + * Returns the longitude, latitude pair (radians) for a given tile in the tessellation. If this is + * the north or south pole the zero vector is returned. This value is also returned if the vnum is + * less than 0 or greater than 3. + * + * @param tile + * @param vnum vertex number of the quadrilateral: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    0upper left (the northwest corner)
    1upper right
    2lower right (southeast corner)
    3lower left
    + * @return returns the longitude, latitude pair (radians) for a given tile in the tessellation + */ + public LatitudinalVector tileVertex(long tile, int vnum) { + return CoordConverters.convertToLatitudinal(tileVertexIJK(tile, vnum)); } - path.closePath(); - Predicate withinPolygon = new Predicate() { - @Override - public boolean test(UnwritableVectorIJK t) { - LatitudinalVector lv = CoordConverters.convertToLatitudinal(t); - Point2D xy = proj.forward(lv); - return path.contains(xy); - } - }; + @Override + public long getTileIndex(LatitudinalVector lv) { + long k = + (long) ((n * FastMath.PI - lv.getLongitude() - 2. * n * lv.getLatitude()) / (2. * FastMath.PI) + 1e-12); + double tt = lv.getLongitude() / (2. * n) + (FastMath.PI / n) * k - FastMath.PI / 2.; + double id = (m + * (FastMath.cos(FastMath.PI / (2. * n)) - FastMath.cos(tt + (n + 1.) * FastMath.PI / (2. * n))) + / (2. * FastMath.cos(FastMath.PI / (2. * n)))) + + 1.; + long result = (long) id; - return matchingTiles(centerTile, withinPolygon); - } - - public static void main(String[] args) { - - AbrateTessellation abt = new AbrateTessellation(100, 12156); - long index = 5604; - - logger.printf(Level.INFO, "numTiles %d n %d m %d", abt.getNumTiles(), abt.n, abt.m); - logger.info("Left " + abt.leftTile(index)); - logger.info("Right " + abt.rightTile(index)); - StringBuffer sb = new StringBuffer(); - for (long tile : abt.aboveTiles(index)) { - sb.append(String.format("%d ", tile)); + return result; } - logger.info("Above " + sb.toString()); - sb = new StringBuffer(); - for (long tile : abt.belowTiles(index)) { - sb.append(String.format("%d ", tile)); + @Override + public LatitudinalVector getTileCenter(long i) { + return CoordConverters.convertToLatitudinal(getTileCenterIJK(i)); } - logger.info("Below " + sb.toString()); - } + /** + * Get the tile center as an {@link UnwritableVectorIJK}. + * + * @param i + * @return + */ + public UnwritableVectorIJK getTileCenterIJK(long i) { + VectorIJK buffer = new VectorIJK(); + if (i == 0) { + buffer.setTo(0, 0, 1); + } else if (i == m + 1) { + buffer.setTo(0, 0, -1); + } else { + for (int vnum = 0; vnum < 4; vnum++) { + buffer.setTo(VectorIJK.add(buffer, tileVertexIJK(i, vnum))); + } + buffer.unitize(); + } + return buffer; + } + /** + * return tile to the left of this tile i.e., adjoining side 3-0 + * + * @param tile + * @return + */ + public long leftTile(long tile) { + long result = tile - 1; + return result < 0 ? 0 : result; + } + + /** + * return tile to the right of this tile i.e., adjoining side 1-2 + * + * @param tile + * @return + */ + public long rightTile(long tile) { + long result = tile + 1; + return result > m + 1 ? m + 1 : result; + } + + /** + * return a sorted set of tiles above or northward of a given tile, i.e., adjoining side 0-1 + * + * @param tile + * @return + */ + public NavigableSet aboveTiles(long tile) { + NavigableSet result = new TreeSet(); + + double ti = tfun(tile); + double ti1 = tfun(tile + 1); + double pin = FastMath.PI / n; + double pi2 = FastMath.PI / 2.; + + if (ti1 < (-pi2 + pin)) { + return result; // nothing north of this + } + LatitudinalVector p2 = gammaLV(ti1 - pin); + long d1 = 0; + /* the small thing is to make handle precision problems */ + double small = pin * 0.05; // this is 0.05 of a tile extent in latitude + + long d2 = getTileIndex(new LatitudinalVector(1, p2.getLatitude() - small, p2.getLongitude())); + if (FastMath.abs(d2 - tile) <= 1) { + d2 = getTileIndex(new LatitudinalVector(1, p2.getLatitude() + small, p2.getLongitude())); + } + if (ti > (-pi2 + pin)) { + LatitudinalVector p1 = gammaLV(ti - pin); + d1 = getTileIndex(new LatitudinalVector(1, p1.getLatitude() - small, p1.getLongitude())); + if (FastMath.abs(d1 - tile) <= 1) { + d1 = getTileIndex(new LatitudinalVector(1, p1.getLatitude() + small, p1.getLongitude())); + } + } + + for (long ic = d1; ic <= d2; ic++) { + result.add(ic); + } + return result; + } + + /** + * return a sorted set of tiles below or southward of a given tile, i.e., adjoining side 2-3 + * + * @param tile + * @return + */ + public NavigableSet belowTiles(long tile) { + NavigableSet result = new TreeSet(); + + double ti = tfun(tile); + double ti1 = tfun(tile + 1); + double pin = FastMath.PI / n; + double pi2 = FastMath.PI / 2.; + + if (tile > m) { + return result; // null + } + if (ti > (pi2 - 2. * pin)) { + result.add(m + 1); + return result; + } + LatitudinalVector p3 = gammaLV(ti1 + pin); + long d4 = m + 1; + /* the small thing is to make handle precision problems */ + double small = pin * 0.05; // this is 0.05 of a tile extent in latitude + + long d3 = getTileIndex(p3); + if (FastMath.abs(d3 - tile) <= 1) { + d3 = getTileIndex(new LatitudinalVector(1, p3.getLatitude() - small, p3.getLongitude())); + } + + if (ti1 < (pi2 - 2. * pin)) { + LatitudinalVector p4 = gammaLV(ti + pin); + d4 = getTileIndex(p4); + if (FastMath.abs(d4 - tile) <= 1) { + d4 = getTileIndex(new LatitudinalVector(1, p4.getLatitude() - small, p4.getLongitude())); + } + + } else { + result.add(m + 1); + return result; + } + + for (long ic = d4; ic <= d3; ic++) { + result.add(ic); + } + + return result; + } + + /** + * add all matching tiles along the spiral to the left and right of tileInRegion + * + * @param matches existing set of matching tiles + * @param tileInRegion starting tile to test + * @param predicate function to test tile center + * @return + */ + private void addLeftAndRightTiles(Set matches, Long tileInRegion, Predicate predicate) { + + if (!matches.contains(tileInRegion) && predicate.test(getTileCenterIJK(tileInRegion))) { + matches.add(tileInRegion); + } + + long tileIndex = rightTile(tileInRegion); + while (true) { + if (!matches.contains(tileIndex)) { + if (!predicate.test(getTileCenterIJK(tileIndex))) { + break; + } + matches.add(tileIndex); + } + tileIndex = rightTile(tileIndex); + // south pole + if (tileIndex == m + 1) { + matches.add(tileIndex); + break; + } + } + + tileIndex = leftTile(tileInRegion); + while (true) { + if (!matches.contains(tileIndex)) { + if (!predicate.test(getTileCenterIJK(tileIndex))) { + break; + } + matches.add(tileIndex); + } + tileIndex = leftTile(tileIndex); + // north pole + if (tileIndex == 0) { + matches.add(tileIndex); + break; + } + } + } + + /** + * Check every tile in the tessellation against the predicate. + * + * @param predicate + * @return + */ + public Set matchingTilesIJK(Predicate predicate) { + Set matches = new HashSet<>(); + + for (long tile = 0; tile < getNumTiles(); tile++) { + if (predicate.test(getTileCenterIJK(tile))) { + matches.add(tile); + } + } + return matches; + } + + /** + * Get the {@link Set} of contiguous tiles which satisfy the predicate. + * + * @param tileInRegion starting tile to test + * @param predicate function to test tile center + * @return set of tiles that satisfy the supplied predicate in the contiguous region including + * tileInRegion + */ + public Set matchingTiles(Long tileInRegion, Predicate predicate) { + Set matches = new HashSet<>(); + + Set tilesInOriginalLine = new HashSet<>(); + addLeftAndRightTiles(tilesInOriginalLine, tileInRegion, predicate); + + Set tilesInLine = tilesInOriginalLine; + matches.addAll(tilesInLine); + + // Add all tiles above this line + while (true) { + if (tilesInLine.size() == 0) { + break; + } + + Set aboveTiles = new HashSet<>(); + for (Long tile : tilesInLine) { + aboveTiles.addAll(aboveTiles(tile)); + } + + tilesInLine = new HashSet<>(); + for (Long tile : aboveTiles) { + addLeftAndRightTiles(tilesInLine, tile, predicate); + } + + // if this is false, then no new tiles were added to matches + if (!matches.addAll(tilesInLine)) { + break; + } + } + + tilesInLine = tilesInOriginalLine; + // Add all tiles below this line + while (true) { + if (tilesInLine.size() == 0) { + break; + } + + Set belowTiles = new HashSet<>(); + for (Long tile : tilesInLine) { + belowTiles.addAll(belowTiles(tile)); + } + + tilesInLine = new HashSet<>(); + for (Long tile : belowTiles) { + addLeftAndRightTiles(tilesInLine, tile, predicate); + } + + // if this is false, then no new tiles were added to matches + if (!matches.addAll(tilesInLine)) { + break; + } + } + + return matches; + } + + /** + * Return the set of tiles whose centers are contained within outline. + * + * @param outline + * @return + */ + public Set tilesWithin(List outline) { + + if (outline.size() == 0) { + return new HashSet<>(); + } + + VectorIJK centerIJK = new VectorIJK(); + for (LatitudinalVector lv : outline) { + centerIJK.setTo(VectorIJK.add(centerIJK, CoordConverters.convert(lv))); + } + centerIJK.scale(1. / outline.size()); + + LatitudinalVector center = CoordConverters.convertToLatitudinal(centerIJK); + Long centerTile = getTileIndex(center); + + StereographicProjection proj = new StereographicProjection(center); + LatitudinalVector first = outline.get(0); + Point2D xy0 = proj.forward(first); + Path2D.Double path = new Path2D.Double(); + path.moveTo(xy0.getX(), xy0.getY()); + for (int i = 1; i < outline.size(); i++) { + Point2D xy = proj.forward(outline.get(i)); + path.lineTo(xy.getX(), xy.getY()); + } + path.closePath(); + + Predicate withinPolygon = new Predicate() { + @Override + public boolean test(UnwritableVectorIJK t) { + LatitudinalVector lv = CoordConverters.convertToLatitudinal(t); + Point2D xy = proj.forward(lv); + return path.contains(xy); + } + }; + + return matchingTiles(centerTile, withinPolygon); + } + + public static void main(String[] args) { + + AbrateTessellation abt = new AbrateTessellation(100, 12156); + long index = 5604; + + logger.printf(Level.INFO, "numTiles %d n %d m %d", abt.getNumTiles(), abt.n, abt.m); + logger.info("Left " + abt.leftTile(index)); + logger.info("Right " + abt.rightTile(index)); + StringBuffer sb = new StringBuffer(); + for (long tile : abt.aboveTiles(index)) { + sb.append(String.format("%d ", tile)); + } + logger.info("Above " + sb.toString()); + + sb = new StringBuffer(); + for (long tile : abt.belowTiles(index)) { + sb.append(String.format("%d ", tile)); + } + logger.info("Below " + sb.toString()); + } } diff --git a/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java b/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java index 11f8ef6..b3d67d8 100644 --- a/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java +++ b/src/main/java/terrasaur/utils/tessellation/FibonacciSphere.java @@ -32,12 +32,11 @@ import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.commons.math3.util.FastMath; import picante.math.coords.CoordConverters; import picante.math.coords.LatitudinalVector; +import picante.math.intervals.UnwritableInterval; import picante.math.vectorspace.MatrixIJ; +import picante.math.vectorspace.UnwritableVectorIJK; import picante.math.vectorspace.VectorIJ; import picante.math.vectorspace.VectorIJK; -import picante.math.vectorspace.UnwritableVectorIJK; - -import picante.math.intervals.UnwritableInterval; /** * Implements fibonacci tiling and reverse lookup as described in @@ -45,275 +44,274 @@ import picante.math.intervals.UnwritableInterval; * Keinert, B., Innmann, M., Sänger, M., Stamminger, M. 2015. Spherical Fibonacci Mapping. ACM * Trans. Graph. 34, 6, Article 193 (November 2015), 7 pages.
    * DOI = 10.1145/2816795.2818131 . - * + * */ public class FibonacciSphere implements SphericalTessellation { - private static final double GOLDEN_RATIO = (Math.sqrt(5) + 1) / 2; + private static final double GOLDEN_RATIO = (Math.sqrt(5) + 1) / 2; - private final List lvList; - private final List ijkList; - private final ThreadLocal> threadLocalDistance; - private final ThreadLocal threadLocalStats; + private final List lvList; + private final List ijkList; + private final ThreadLocal> threadLocalDistance; + private final ThreadLocal threadLocalStats; - /** - * Create a set of points approximately uniformly distributed about the unit sphere - * - * @param npts Number of points to use - */ - public FibonacciSphere(int npts) { - lvList = new ArrayList<>(); - ijkList = new ArrayList<>(); - threadLocalStats = new ThreadLocal<>(); + /** + * Create a set of points approximately uniformly distributed about the unit sphere + * + * @param npts Number of points to use + */ + public FibonacciSphere(int npts) { + lvList = new ArrayList<>(); + ijkList = new ArrayList<>(); + threadLocalStats = new ThreadLocal<>(); - for (int i = 0; i < npts; i++) { - double phi = 2 * Math.PI * madfrac(i, GOLDEN_RATIO - 1); - double z = 1 - (2 * i + 1.) / npts; + for (int i = 0; i < npts; i++) { + double phi = 2 * Math.PI * madfrac(i, GOLDEN_RATIO - 1); + double z = 1 - (2 * i + 1.) / npts; - LatitudinalVector lv = new LatitudinalVector(1, Math.asin(z), phi); + LatitudinalVector lv = new LatitudinalVector(1, Math.asin(z), phi); - lvList.add(lv); - ijkList.add(CoordConverters.convert(lv)); - } - - threadLocalDistance = new ThreadLocal<>(); - } - - /** - * Get statistics on the distances between each point and its closest neighbor - * - * @return statistics on the distances between each point and its closest neighbor - */ - public DescriptiveStatistics getDistanceStats() { - if (threadLocalStats.get() == null) { - DescriptiveStatistics distanceStats = new DescriptiveStatistics(); - for (Double dist : getClosestNeighborDistance()) { - distanceStats.addValue(Math.toDegrees(dist)); - } - threadLocalStats.set(distanceStats); - } - return threadLocalStats.get(); - } - - private List getClosestNeighborDistance() { - if (threadLocalDistance.get() == null) { - List closestNeighborDistance = new ArrayList<>(); - for (int i = 0; i < lvList.size(); i++) { - - double minDist = Double.MAX_VALUE; - - for (UnwritableVectorIJK ijk : ijkList) { - double dist = ijk.getSeparation(ijkList.get(i)); - if (dist > 0 && dist < minDist) { - minDist = dist; - } + lvList.add(lv); + ijkList.add(CoordConverters.convert(lv)); } - closestNeighborDistance.add(minDist); - } - threadLocalDistance.set(closestNeighborDistance); - } - return threadLocalDistance.get(); - } - @Override - public long getNumTiles() { - return lvList.size(); - } - - /** - * @param i tile index - * @return position of this tile center as a LatitudinalVector - */ - @Override - public LatitudinalVector getTileCenter(long i) { - return lvList.get((int) i); - } - - /** - * - * @param i tile index - * @return position of this tile center as a VectorIJK - */ - public UnwritableVectorIJK getTileCenterIJK(long i) { - return ijkList.get((int) i); - } - - @Override - public long getTileIndex(LatitudinalVector lv) { - return getTileIndex(CoordConverters.convert(lv)); - } - - @Override - public long getTileIndex(UnwritableVectorIJK ijk) { - return getNearest(ijk).getValue(); - /*- - final long n = getNumTiles(); - final double rcpN = 1. / n; - - double phi = Math.min(Math.atan2(p.getJ(), p.getI()), Math.PI); - double cosTheta = p.getK(); // theta is the colatitude, cosTheta is the sine of the latitude - - // global coordinates of the input point. (0,0) maps - // to first point in the Fibonacci set. - VectorIJ uv = new VectorIJ(phi, cosTheta - (1 - rcpN)); - - MatrixIJ B = getLocalToGlobalTransform(p); - MatrixIJ invB = B.createInverse(); - - // coordinates on the local grid - VectorIJ c = invB.mxv(uv); - VectorIJ corner = new VectorIJ(Math.floor(c.getI()), Math.floor(c.getJ())); - - UnwritableInterval cosThetaRange = new UnwritableInterval(-1, 1); - - // global coordinates of the corner - VectorIJ thisUV = B.mxv(corner); - cosTheta = thisUV.getJ() + (1 - rcpN); - cosTheta = cosThetaRange.clamp(cosTheta) * 2 - cosTheta; - - // index of the point with the closest latitude - long i = (long) Math.floor(n * 0.5 * (1 - cosTheta)); - return i; - */ - } - - private double madfrac(double a, double b) { - return a * b - FastMath.floor(a * b); - } - - /** - * Get the nearest point from the desired location. - * - * @param lv input location - * @return key is distance in radians, value is point index - */ - public Map.Entry getNearest(LatitudinalVector lv) { - return getNearest(CoordConverters.convert(lv)); - } - - /** - * Get the nearest point from the input location. Do the inverse mapping using the method of - *

    - * Keinert, B., Innmann, M., Sänger, M., Stamminger, M. 2015. Spherical Fibonacci Mapping. ACM - * Trans. Graph. 34, 6, Article 193 (November 2015), 7 pages.
    - * DOI = 10.1145/2816795.2818131 . - * - * - * @param ijk cartesian coordinates - * @return key is distance in radians, value is point index - */ - public Map.Entry getNearest(UnwritableVectorIJK ijk) { - final long n = getNumTiles(); - final double rcpN = 1. / n; - - double phi = Math.min(Math.atan2(ijk.getJ(), ijk.getI()), Math.PI); - double cosTheta = ijk.getK(); // theta is the colatitude, cosTheta is the sine of the latitude - - // global coordinates of the input point. (0,0) maps - // to first point in the Fibonacci set. - VectorIJ uv = new VectorIJ(phi, cosTheta - (1 - rcpN)); - - MatrixIJ B = getLocalToGlobalTransform(ijk); - MatrixIJ invB = B.createInverse(); - - // coordinates on the local grid - VectorIJ c = invB.mxv(uv); - c = new VectorIJ(Math.floor(c.getI()), Math.floor(c.getJ())); - - double d = Double.MAX_VALUE; - long j = 0; - - UnwritableInterval cosThetaRange = new UnwritableInterval(-1, 1); - for (int s = 0; s < 4; s++) { - VectorIJ corner = VectorIJ.add(new VectorIJ(s % 2, s / 2), c); - - VectorIJ thisUV = B.mxv(corner); - cosTheta = thisUV.getJ() + (1 - rcpN); - cosTheta = cosThetaRange.clamp(cosTheta) * 2 - cosTheta; - - // index of the point with the closest latitude - long i = (long) Math.floor(n * 0.5 * (1 - cosTheta)); - - phi = 2 * Math.PI * madfrac(i, GOLDEN_RATIO - 1); - cosTheta = 1 - (2 * i + 1) * rcpN; - double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); // theta is the colatitude (90 - - // latitude) - - VectorIJK q = new VectorIJK(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta); - VectorIJK qMp = VectorIJK.subtract(q, ijk); - - double dist2 = qMp.getDot(qMp); - - if (dist2 < d) { - d = dist2; - j = i; - } + threadLocalDistance = new ThreadLocal<>(); } - return new AbstractMap.SimpleEntry(Math.sqrt(d), j); - } - - private MatrixIJ getLocalToGlobalTransform(UnwritableVectorIJK p) { - final long n = getNumTiles(); - - double cosTheta = p.getK(); // theta is the colatitude, cosTheta is the sine of the latitude - - // k is the zone number (Equation 5) - int k = (int) Math.max(2, - Math.floor(Math.log(n * Math.PI * Math.sqrt(5) * (1 - cosTheta * cosTheta)) - / Math.log(GOLDEN_RATIO + 1))); - - // estimate of the fibonacci number for this zone - double Fk = Math.pow(GOLDEN_RATIO, k) / Math.sqrt(5); - - // F0 and F1 are bounds on the fibonacci number for this zone - double F0 = Math.round(Fk); - double F1 = Math.round(Fk * GOLDEN_RATIO); - - // basis vectors for the local grid - VectorIJ bk = new VectorIJ( - 2 * Math.PI * (madfrac(F0 + 1, GOLDEN_RATIO - 1) - (GOLDEN_RATIO - 1)), -2 * F0 / n); - VectorIJ bkp = new VectorIJ( - 2 * Math.PI * (madfrac(F1 + 1, GOLDEN_RATIO - 1) - (GOLDEN_RATIO - 1)), -2 * F1 / n); - - // local to global transformation matrix - MatrixIJ B = new MatrixIJ(bk, bkp); - - return B; - } - - /** - * - * @param i point index - * @return distance to this point's closest neighbor - */ - public Double getDist(int i) { - return getClosestNeighborDistance().get(i); - } - - /** - * - * @param lv input point - * @return map of points sorted by distance from the input point - */ - public NavigableMap getDistanceMap(LatitudinalVector lv) { - UnwritableVectorIJK ijk = CoordConverters.convert(lv); - return getDistanceMap(ijk); - } - - /** - * - * @param point input point - * @return map of tile indices sorted by distance from the input point - */ - public NavigableMap getDistanceMap(UnwritableVectorIJK point) { - NavigableMap distanceMap = new TreeMap<>(); - for (int i = 0; i < ijkList.size(); i++) { - UnwritableVectorIJK ijk = ijkList.get(i); - double dist = ijk.getSeparation(point); - distanceMap.put(dist, i); + /** + * Get statistics on the distances between each point and its closest neighbor + * + * @return statistics on the distances between each point and its closest neighbor + */ + public DescriptiveStatistics getDistanceStats() { + if (threadLocalStats.get() == null) { + DescriptiveStatistics distanceStats = new DescriptiveStatistics(); + for (Double dist : getClosestNeighborDistance()) { + distanceStats.addValue(Math.toDegrees(dist)); + } + threadLocalStats.set(distanceStats); + } + return threadLocalStats.get(); } - return distanceMap; - } + private List getClosestNeighborDistance() { + if (threadLocalDistance.get() == null) { + List closestNeighborDistance = new ArrayList<>(); + for (int i = 0; i < lvList.size(); i++) { + + double minDist = Double.MAX_VALUE; + + for (UnwritableVectorIJK ijk : ijkList) { + double dist = ijk.getSeparation(ijkList.get(i)); + if (dist > 0 && dist < minDist) { + minDist = dist; + } + } + closestNeighborDistance.add(minDist); + } + threadLocalDistance.set(closestNeighborDistance); + } + return threadLocalDistance.get(); + } + + @Override + public long getNumTiles() { + return lvList.size(); + } + + /** + * @param i tile index + * @return position of this tile center as a LatitudinalVector + */ + @Override + public LatitudinalVector getTileCenter(long i) { + return lvList.get((int) i); + } + + /** + * + * @param i tile index + * @return position of this tile center as a VectorIJK + */ + public UnwritableVectorIJK getTileCenterIJK(long i) { + return ijkList.get((int) i); + } + + @Override + public long getTileIndex(LatitudinalVector lv) { + return getTileIndex(CoordConverters.convert(lv)); + } + + @Override + public long getTileIndex(UnwritableVectorIJK ijk) { + return getNearest(ijk).getValue(); + /*- + final long n = getNumTiles(); + final double rcpN = 1. / n; + + double phi = Math.min(Math.atan2(p.getJ(), p.getI()), Math.PI); + double cosTheta = p.getK(); // theta is the colatitude, cosTheta is the sine of the latitude + + // global coordinates of the input point. (0,0) maps + // to first point in the Fibonacci set. + VectorIJ uv = new VectorIJ(phi, cosTheta - (1 - rcpN)); + + MatrixIJ B = getLocalToGlobalTransform(p); + MatrixIJ invB = B.createInverse(); + + // coordinates on the local grid + VectorIJ c = invB.mxv(uv); + VectorIJ corner = new VectorIJ(Math.floor(c.getI()), Math.floor(c.getJ())); + + UnwritableInterval cosThetaRange = new UnwritableInterval(-1, 1); + + // global coordinates of the corner + VectorIJ thisUV = B.mxv(corner); + cosTheta = thisUV.getJ() + (1 - rcpN); + cosTheta = cosThetaRange.clamp(cosTheta) * 2 - cosTheta; + + // index of the point with the closest latitude + long i = (long) Math.floor(n * 0.5 * (1 - cosTheta)); + return i; + */ + } + + private double madfrac(double a, double b) { + return a * b - FastMath.floor(a * b); + } + + /** + * Get the nearest point from the desired location. + * + * @param lv input location + * @return key is distance in radians, value is point index + */ + public Map.Entry getNearest(LatitudinalVector lv) { + return getNearest(CoordConverters.convert(lv)); + } + + /** + * Get the nearest point from the input location. Do the inverse mapping using the method of + *

    + * Keinert, B., Innmann, M., Sänger, M., Stamminger, M. 2015. Spherical Fibonacci Mapping. ACM + * Trans. Graph. 34, 6, Article 193 (November 2015), 7 pages.
    + * DOI = 10.1145/2816795.2818131 . + * + * + * @param ijk cartesian coordinates + * @return key is distance in radians, value is point index + */ + public Map.Entry getNearest(UnwritableVectorIJK ijk) { + final long n = getNumTiles(); + final double rcpN = 1. / n; + + double phi = Math.min(Math.atan2(ijk.getJ(), ijk.getI()), Math.PI); + double cosTheta = ijk.getK(); // theta is the colatitude, cosTheta is the sine of the latitude + + // global coordinates of the input point. (0,0) maps + // to first point in the Fibonacci set. + VectorIJ uv = new VectorIJ(phi, cosTheta - (1 - rcpN)); + + MatrixIJ B = getLocalToGlobalTransform(ijk); + MatrixIJ invB = B.createInverse(); + + // coordinates on the local grid + VectorIJ c = invB.mxv(uv); + c = new VectorIJ(Math.floor(c.getI()), Math.floor(c.getJ())); + + double d = Double.MAX_VALUE; + long j = 0; + + UnwritableInterval cosThetaRange = new UnwritableInterval(-1, 1); + for (int s = 0; s < 4; s++) { + VectorIJ corner = VectorIJ.add(new VectorIJ(s % 2, s / 2), c); + + VectorIJ thisUV = B.mxv(corner); + cosTheta = thisUV.getJ() + (1 - rcpN); + cosTheta = cosThetaRange.clamp(cosTheta) * 2 - cosTheta; + + // index of the point with the closest latitude + long i = (long) Math.floor(n * 0.5 * (1 - cosTheta)); + + phi = 2 * Math.PI * madfrac(i, GOLDEN_RATIO - 1); + cosTheta = 1 - (2 * i + 1) * rcpN; + double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); // theta is the colatitude (90 - + // latitude) + + VectorIJK q = new VectorIJK(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta); + VectorIJK qMp = VectorIJK.subtract(q, ijk); + + double dist2 = qMp.getDot(qMp); + + if (dist2 < d) { + d = dist2; + j = i; + } + } + + return new AbstractMap.SimpleEntry(Math.sqrt(d), j); + } + + private MatrixIJ getLocalToGlobalTransform(UnwritableVectorIJK p) { + final long n = getNumTiles(); + + double cosTheta = p.getK(); // theta is the colatitude, cosTheta is the sine of the latitude + + // k is the zone number (Equation 5) + int k = (int) Math.max( + 2, + Math.floor( + Math.log(n * Math.PI * Math.sqrt(5) * (1 - cosTheta * cosTheta)) / Math.log(GOLDEN_RATIO + 1))); + + // estimate of the fibonacci number for this zone + double Fk = Math.pow(GOLDEN_RATIO, k) / Math.sqrt(5); + + // F0 and F1 are bounds on the fibonacci number for this zone + double F0 = Math.round(Fk); + double F1 = Math.round(Fk * GOLDEN_RATIO); + + // basis vectors for the local grid + VectorIJ bk = new VectorIJ(2 * Math.PI * (madfrac(F0 + 1, GOLDEN_RATIO - 1) - (GOLDEN_RATIO - 1)), -2 * F0 / n); + VectorIJ bkp = + new VectorIJ(2 * Math.PI * (madfrac(F1 + 1, GOLDEN_RATIO - 1) - (GOLDEN_RATIO - 1)), -2 * F1 / n); + + // local to global transformation matrix + MatrixIJ B = new MatrixIJ(bk, bkp); + + return B; + } + + /** + * + * @param i point index + * @return distance to this point's closest neighbor + */ + public Double getDist(int i) { + return getClosestNeighborDistance().get(i); + } + + /** + * + * @param lv input point + * @return map of points sorted by distance from the input point + */ + public NavigableMap getDistanceMap(LatitudinalVector lv) { + UnwritableVectorIJK ijk = CoordConverters.convert(lv); + return getDistanceMap(ijk); + } + + /** + * + * @param point input point + * @return map of tile indices sorted by distance from the input point + */ + public NavigableMap getDistanceMap(UnwritableVectorIJK point) { + NavigableMap distanceMap = new TreeMap<>(); + for (int i = 0; i < ijkList.size(); i++) { + UnwritableVectorIJK ijk = ijkList.get(i); + double dist = ijk.getSeparation(point); + distanceMap.put(dist, i); + } + return distanceMap; + } } diff --git a/src/main/java/terrasaur/utils/tessellation/SphericalTessellation.java b/src/main/java/terrasaur/utils/tessellation/SphericalTessellation.java index 49cb29d..79e0d07 100644 --- a/src/main/java/terrasaur/utils/tessellation/SphericalTessellation.java +++ b/src/main/java/terrasaur/utils/tessellation/SphericalTessellation.java @@ -22,63 +22,61 @@ */ package terrasaur.utils.tessellation; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; import picante.math.coords.CoordConverters; import picante.math.coords.LatitudinalVector; import picante.math.vectorspace.UnwritableVectorIJK; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Predicate; - public interface SphericalTessellation { - /** - * Number of tiles in the tessellation. - * - * @return - */ - public long getNumTiles(); + /** + * Number of tiles in the tessellation. + * + * @return + */ + public long getNumTiles(); - /** - * Return the index of the tile containing the unit vector ijk. - * - * @param ijk - * @return - */ - public default long getTileIndex(UnwritableVectorIJK ijk) { - return getTileIndex(CoordConverters.convertToLatitudinal(ijk)); - } - - /** - * Return the index of the tile containing the unit vector lv. - * - * @param lv - * @return - */ - public long getTileIndex(LatitudinalVector lv); - - /** - * Return the unit latitudinal vector pointing to the tile center with index i. - * - * @param i - * @return - */ - public LatitudinalVector getTileCenter(long i); - - /** - * Return the set of tiles that satisfy the supplied predicate. - * - * @param predicate - * @return - */ - public default Set matchingTiles(Predicate predicate) { - Set matches = new HashSet<>(); - for (long i = 0; i < getNumTiles(); i++) { - if (predicate.test(getTileCenter(i))) { - matches.add(i); - } + /** + * Return the index of the tile containing the unit vector ijk. + * + * @param ijk + * @return + */ + public default long getTileIndex(UnwritableVectorIJK ijk) { + return getTileIndex(CoordConverters.convertToLatitudinal(ijk)); } - return matches; - } + /** + * Return the index of the tile containing the unit vector lv. + * + * @param lv + * @return + */ + public long getTileIndex(LatitudinalVector lv); + + /** + * Return the unit latitudinal vector pointing to the tile center with index i. + * + * @param i + * @return + */ + public LatitudinalVector getTileCenter(long i); + + /** + * Return the set of tiles that satisfy the supplied predicate. + * + * @param predicate + * @return + */ + public default Set matchingTiles(Predicate predicate) { + Set matches = new HashSet<>(); + for (long i = 0; i < getNumTiles(); i++) { + if (predicate.test(getTileCenter(i))) { + matches.add(i); + } + } + return matches; + } } diff --git a/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java b/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java index 73e0ae6..3d1eb44 100644 --- a/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java +++ b/src/main/java/terrasaur/utils/tessellation/StereographicProjection.java @@ -22,112 +22,109 @@ */ package terrasaur.utils.tessellation; +import java.awt.geom.Point2D; import picante.math.coords.CoordConverters; import picante.math.coords.LatitudinalVector; import picante.math.vectorspace.UnwritableVectorIJK; -import java.awt.geom.Point2D; - /** * Implement a stereographic projection. Based on Snyder (1987). - * + * * @author nairah1 * */ public class StereographicProjection { - private final double centerLat, centerLon; - private final double sinCenterLat, cosCenterLat; - private final double k0, R; + private final double centerLat, centerLon; + private final double sinCenterLat, cosCenterLat; + private final double k0, R; - /** - * Create a stereographic projection centered on the desired coordinates. The radius of the - * projection is 1. - * - * @param center projection center - */ - public StereographicProjection(LatitudinalVector center) { - this(1.0, center); - } - - /** - * Create a stereographic projection centered on the desired coordinates. The radius of the - * projection may be specified. - * - * @param R scale - * @param center projection center - */ - public StereographicProjection(double R, LatitudinalVector center) { - this.R = R; - this.centerLat = center.getLatitude(); - this.centerLon = center.getLongitude(); - - sinCenterLat = Math.sin(centerLat); - cosCenterLat = Math.cos(centerLat); - k0 = 1.0; - } - - /** - * Return x, y coordinates of the input coordinates. - * - * @param xyz 3D input coordinate - * @return 2D projected coordinate - */ - public Point2D forward(UnwritableVectorIJK xyz) { - LatitudinalVector lv = CoordConverters.convertToLatitudinal(xyz); - return forward(lv); - } - - /** - * Return x, y coordinates of the input coordinates. - * - * @param lv 3D input coordinate (radius is not used - only lat and lon) - * @return 2D projected coordinate - */ - public Point2D forward(LatitudinalVector lv) { - return forward(lv.getLatitude(), lv.getLongitude()); - } - - /** - * Return x, y coordinates of the input coordinates. - * - * @param lat latitude (radians) - * @param lon longitude (radians) - * @return 2D projected coordinate - */ - public Point2D forward(double lat, double lon) { - double sinLat = Math.sin(lat); - double cosLat = Math.cos(lat); - double sinLon = Math.sin(lon - centerLon); - double cosLon = Math.cos(lon - centerLon); - - double k = 2 * k0 / (1 + sinCenterLat * sinLat + cosCenterLat * cosLat * cosLon); - double x = R * k * cosLat * sinLon; - double y = R * k * (cosCenterLat * sinLat - sinCenterLat * cosLat * cosLon); - - return new Point2D.Double(x, y); - } - - public LatitudinalVector inverse(double x, double y) { - final double rho = Math.sqrt(x * x + y * y); - double c = 2 * Math.atan(rho / (2 * R * k0)); - double sinC = Math.sin(c); - double cosC = Math.cos(c); - - double lat = - rho == 0 ? centerLat : Math.asin(cosC * sinCenterLat + y * sinC * cosCenterLat / rho); - double lon = centerLon; - if (rho != 0) { - if (sinCenterLat == 1) { - lon += Math.atan(-x / y); - } else if (sinCenterLat == -1) { - lon += Math.atan(x / y); - } else { - lon += Math.atan(x * sinC / (rho * cosCenterLat * cosC - y * sinCenterLat * sinC)); - } + /** + * Create a stereographic projection centered on the desired coordinates. The radius of the + * projection is 1. + * + * @param center projection center + */ + public StereographicProjection(LatitudinalVector center) { + this(1.0, center); } - return new LatitudinalVector(1.0, lat, lon); - } + /** + * Create a stereographic projection centered on the desired coordinates. The radius of the + * projection may be specified. + * + * @param R scale + * @param center projection center + */ + public StereographicProjection(double R, LatitudinalVector center) { + this.R = R; + this.centerLat = center.getLatitude(); + this.centerLon = center.getLongitude(); + sinCenterLat = Math.sin(centerLat); + cosCenterLat = Math.cos(centerLat); + k0 = 1.0; + } + + /** + * Return x, y coordinates of the input coordinates. + * + * @param xyz 3D input coordinate + * @return 2D projected coordinate + */ + public Point2D forward(UnwritableVectorIJK xyz) { + LatitudinalVector lv = CoordConverters.convertToLatitudinal(xyz); + return forward(lv); + } + + /** + * Return x, y coordinates of the input coordinates. + * + * @param lv 3D input coordinate (radius is not used - only lat and lon) + * @return 2D projected coordinate + */ + public Point2D forward(LatitudinalVector lv) { + return forward(lv.getLatitude(), lv.getLongitude()); + } + + /** + * Return x, y coordinates of the input coordinates. + * + * @param lat latitude (radians) + * @param lon longitude (radians) + * @return 2D projected coordinate + */ + public Point2D forward(double lat, double lon) { + double sinLat = Math.sin(lat); + double cosLat = Math.cos(lat); + double sinLon = Math.sin(lon - centerLon); + double cosLon = Math.cos(lon - centerLon); + + double k = 2 * k0 / (1 + sinCenterLat * sinLat + cosCenterLat * cosLat * cosLon); + double x = R * k * cosLat * sinLon; + double y = R * k * (cosCenterLat * sinLat - sinCenterLat * cosLat * cosLon); + + return new Point2D.Double(x, y); + } + + public LatitudinalVector inverse(double x, double y) { + final double rho = Math.sqrt(x * x + y * y); + double c = 2 * Math.atan(rho / (2 * R * k0)); + double sinC = Math.sin(c); + double cosC = Math.cos(c); + + double lat = rho == 0 ? centerLat : Math.asin(cosC * sinCenterLat + y * sinC * cosCenterLat / rho); + double lon = centerLon; + if (rho != 0) { + if (sinCenterLat == 1) { + lon += Math.atan(-x / y); + } else if (sinCenterLat == -1) { + lon += Math.atan(x / y); + } else { + lon += Math.atan(x * sinC / (rho * cosCenterLat * cosC - y * sinCenterLat * sinC)); + } + } + + return new LatitudinalVector(1.0, lat, lon); + } } diff --git a/src/main/java/terrasaur/utils/xml/AsciiFile.java b/src/main/java/terrasaur/utils/xml/AsciiFile.java index e984dbf..c9e161b 100644 --- a/src/main/java/terrasaur/utils/xml/AsciiFile.java +++ b/src/main/java/terrasaur/utils/xml/AsciiFile.java @@ -39,102 +39,101 @@ import java.util.List; */ public class AsciiFile { - private FileOutputStream fstreamOut; - private PrintStream printStream; + private FileOutputStream fstreamOut; + private PrintStream printStream; - public AsciiFile(String outFname) { - fstreamOut = null; - try { - fstreamOut = new FileOutputStream(outFname); - printStream = new PrintStream(fstreamOut); - } catch (IOException e) { - System.err.println("Cannot open file for writing:" + outFname); - System.exit(1); - } - } - - /** - * Print the double array to the ascii file. Uses default formatting. - * - * @param arrayD - */ - public void ArrayDToFile(double[] arrayD) { - String line; - String terminator = "\n"; - - for (int ii = 0; ii < arrayD.length; ii++) { - line = Double.toString(arrayD[ii]); - printStream.print(line + terminator); - } - } - - /** - * Print a given string as a record to the file. Use this when it becomes cumbersome or redundant - * to form the List before writing the list to a file. This way I can dynamically form - * the string in a loop and write each string as it is created. - * - * @param record - * @param termType - */ - public void streamSToFile(String record, int termType) { - String terminator; - - switch (termType) { - case -1: - terminator = ""; - case 0: - terminator = "\r\n"; - break; - case 1: - terminator = "\n"; - break; - default: - terminator = "\n"; - } - printStream.print(record + terminator); - } - - /** - * Print each string in the string list to the PrintStream. Terminate each string with \r\n. Note: - * List should be backed by LinkedList in order to preserve ordering of strings. - * - * @param stringList - * @param termType - */ - public void lStringToFile(List stringList, int termType) { - - String terminator; - - switch (termType) { - case 0: - terminator = "\r\n"; - break; - case 1: - terminator = "\n"; - break; - default: - terminator = "\n"; - } - for (String line : stringList) { - printStream.print(line + terminator); - } - } - - public void closeFile() { - printStream.close(); - } - - public static List readFileasStrList(String path) - throws FileNotFoundException, IOException { - List fileContent = new ArrayList(); - String thisLine = null; - try (BufferedReader br = new BufferedReader(new FileReader(path))) { - - while ((thisLine = br.readLine()) != null) { - fileContent.add(thisLine); - } + public AsciiFile(String outFname) { + fstreamOut = null; + try { + fstreamOut = new FileOutputStream(outFname); + printStream = new PrintStream(fstreamOut); + } catch (IOException e) { + System.err.println("Cannot open file for writing:" + outFname); + System.exit(1); + } } - return fileContent; - } + /** + * Print the double array to the ascii file. Uses default formatting. + * + * @param arrayD + */ + public void ArrayDToFile(double[] arrayD) { + String line; + String terminator = "\n"; + + for (int ii = 0; ii < arrayD.length; ii++) { + line = Double.toString(arrayD[ii]); + printStream.print(line + terminator); + } + } + + /** + * Print a given string as a record to the file. Use this when it becomes cumbersome or redundant + * to form the List before writing the list to a file. This way I can dynamically form + * the string in a loop and write each string as it is created. + * + * @param record + * @param termType + */ + public void streamSToFile(String record, int termType) { + String terminator; + + switch (termType) { + case -1: + terminator = ""; + case 0: + terminator = "\r\n"; + break; + case 1: + terminator = "\n"; + break; + default: + terminator = "\n"; + } + printStream.print(record + terminator); + } + + /** + * Print each string in the string list to the PrintStream. Terminate each string with \r\n. Note: + * List should be backed by LinkedList in order to preserve ordering of strings. + * + * @param stringList + * @param termType + */ + public void lStringToFile(List stringList, int termType) { + + String terminator; + + switch (termType) { + case 0: + terminator = "\r\n"; + break; + case 1: + terminator = "\n"; + break; + default: + terminator = "\n"; + } + for (String line : stringList) { + printStream.print(line + terminator); + } + } + + public void closeFile() { + printStream.close(); + } + + public static List readFileasStrList(String path) throws FileNotFoundException, IOException { + List fileContent = new ArrayList(); + String thisLine = null; + try (BufferedReader br = new BufferedReader(new FileReader(path))) { + + while ((thisLine = br.readLine()) != null) { + fileContent.add(thisLine); + } + } + + return fileContent; + } } diff --git a/src/test/java/terrasaur/smallBodyModel/SBMTStructureTest.java b/src/test/java/terrasaur/smallBodyModel/SBMTStructureTest.java index fb04a59..d918290 100644 --- a/src/test/java/terrasaur/smallBodyModel/SBMTStructureTest.java +++ b/src/test/java/terrasaur/smallBodyModel/SBMTStructureTest.java @@ -26,16 +26,15 @@ import org.junit.Test; public class SBMTStructureTest { - @Test - public void test() { + @Test + public void test() { - String line = - "1 default -261.61622569431825 2.0874520245281474 -1.3419589274656403 -0.2938864395072801 179.542843137706 261.6279951692062 NA NA NA NA 0.4 1.0 0.0 255,0,255 \"\""; + String line = + "1 default -261.61622569431825 2.0874520245281474 -1.3419589274656403 -0.2938864395072801 179.542843137706 261.6279951692062 NA NA NA NA 0.4 1.0 0.0 255,0,255 \"\""; - SBMTStructure s = SBMTStructure.fromString(line); + SBMTStructure s = SBMTStructure.fromString(line); - System.out.println(line); - System.out.println(s.toString()); - - } + System.out.println(line); + System.out.println(s.toString()); + } } diff --git a/src/test/java/terrasaur/utils/Binary16Test.java b/src/test/java/terrasaur/utils/Binary16Test.java index 918fa6a..44b9a3d 100644 --- a/src/test/java/terrasaur/utils/Binary16Test.java +++ b/src/test/java/terrasaur/utils/Binary16Test.java @@ -27,18 +27,17 @@ import org.junit.Test; public class Binary16Test { - @Test - public void TestBinary16() { - Random r = new Random(); + @Test + public void TestBinary16() { + Random r = new Random(); - for (int i = 0; i < 30; i++) { - // generate a random number between -1e6 and 1e6 - float f = (float) (Math.pow(10, -12 * r.nextDouble() + 6) * Math.pow(-1, r.nextInt())); - int fromf = Binary16.fromFloat(f); - float tof = Binary16.toFloat(fromf); + for (int i = 0; i < 30; i++) { + // generate a random number between -1e6 and 1e6 + float f = (float) (Math.pow(10, -12 * r.nextDouble() + 6) * Math.pow(-1, r.nextInt())); + int fromf = Binary16.fromFloat(f); + float tof = Binary16.toFloat(fromf); - // System.out.printf("%16.6g %8d %16.6g\n", f, fromf, tof); + // System.out.printf("%16.6g %8d %16.6g\n", f, fromf, tof); + } } - } - } diff --git a/src/test/java/terrasaur/utils/FitSurfaceTest.java b/src/test/java/terrasaur/utils/FitSurfaceTest.java index e10f115..c692995 100644 --- a/src/test/java/terrasaur/utils/FitSurfaceTest.java +++ b/src/test/java/terrasaur/utils/FitSurfaceTest.java @@ -25,9 +25,9 @@ package terrasaur.utils; import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; +import net.jafama.FastMath; import org.apache.commons.math3.analysis.MultivariateFunction; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; -import net.jafama.FastMath; import org.junit.Ignore; import org.junit.Test; import terrasaur.utils.saaPlotLib.canvas.AreaPlot; @@ -43,116 +43,127 @@ import terrasaur.utils.saaPlotLib.util.StringFunctions; public class FitSurfaceTest { - /** - * plot a multivariate function compared with its fit - */ - @Test - @Ignore - public void test01() { - int degree = 5; // degree 6 is a worse fit - int numPts = (degree + 1) * (degree + 1); - numPts = 61; - double maxX = numPts / 10.; + /** + * plot a multivariate function compared with its fit + */ + @Test + @Ignore + public void test01() { + int degree = 5; // degree 6 is a worse fit + int numPts = (degree + 1) * (degree + 1); + numPts = 61; + double maxX = numPts / 10.; - double[][] x = new double[numPts][numPts]; - double[][] y = new double[numPts][numPts]; - double[][] f = new double[numPts][numPts]; + double[][] x = new double[numPts][numPts]; + double[][] y = new double[numPts][numPts]; + double[][] f = new double[numPts][numPts]; - for (int i = 0; i < numPts; i++) { - for (int j = 0; j < numPts; j++) { - x[i][j] = i * maxX / (numPts - 1.); - y[j][i] = x[i][j]; - } + for (int i = 0; i < numPts; i++) { + for (int j = 0; j < numPts; j++) { + x[i][j] = i * maxX / (numPts - 1.); + y[j][i] = x[i][j]; + } + } + + MultivariateFunction func = new MultivariateFunction() { + @Override + public double value(double[] point) { + return -FastMath.sin(2 * point[0]) + FastMath.cos(point[1] / 2); + } + }; + + List points = new ArrayList<>(); + for (int i = 0; i < numPts; i++) { + for (int j = 0; j < numPts; j++) { + double[] point = {x[i][j], y[i][j]}; + f[i][j] = func.value(point); + points.add(new Vector3D(x[i][j], y[i][j], f[i][j])); + } + } + + FitSurface app = new FitSurface(points, degree); + + MultivariateFunction fit = new MultivariateFunction() { + @Override + public double value(double[] point) { + return app.value(point[0], point[1]); + } + }; + + List surfaceFit = new ArrayList<>(); + for (int i = 0; i < numPts; i++) { + for (int j = 0; j < numPts; j++) { + double[] point = {x[i][j], y[i][j]}; + f[i][j] = fit.value(point); + surfaceFit.add(new Vector3D(x[i][j], y[i][j], f[i][j])); + } + } + + MultivariateFunction diff = new MultivariateFunction() { + @Override + public double value(double[] point) { + return func.value(point) - fit.value(point); + } + }; + + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .title("Func") + .build(); + + AxisX xLowerAxis = new AxisX(0, maxX, "X (degrees)", StringFunctions.toDegrees("%.0f")); + AxisY yLeftAxis = new AxisY(0, maxX, "Y (degrees)", StringFunctions.toDegrees("%.0f")); + + ColorRamp ramp = ColorRamp.create(TYPE.PARULA, -1, 1); + + AreaPlot canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + canvas.plot(func, ramp, xLowerAxis, yLeftAxis); + // Vertical, along right side + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) + .ramp(ramp) + .numTicks(9) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build()); + + PlotCanvas.showJFrame(canvas.getImage()); + + config = ImmutablePlotConfig.copyOf(config).withTitle("Fit"); + canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + canvas.plot(fit, ramp, xLowerAxis, yLeftAxis); + // Vertical, along right side + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) + .ramp(ramp) + .numTicks(9) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build()); + + PlotCanvas.showJFrame(canvas.getImage()); + + config = ImmutablePlotConfig.copyOf(config).withTitle("Func-Fit"); + ramp = ColorRamp.create(TYPE.PARULA, -1, 1); + ramp.addLimitColors(); + canvas = new AreaPlot(config); + canvas.setAxes(xLowerAxis, yLeftAxis); + canvas.drawAxes(); + canvas.plot(diff, ramp, xLowerAxis, yLeftAxis); + // Vertical, along right side + canvas.drawColorBar(ImmutableColorBar.builder() + .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) + .ramp(ramp) + .numTicks(9) + .tickFunction(StringFunctions.fixedFormat("%.2f")) + .build()); + + PlotCanvas.showJFrame(canvas.getImage()); + + PolyDataUtil.writeVTKPoints("points.vtk", points); + PolyDataUtil.writeVTKPoints("surface" + degree + ".vtk", surfaceFit); } - - MultivariateFunction func = new MultivariateFunction() { - @Override - public double value(double[] point) { - return -FastMath.sin(2 * point[0]) + FastMath.cos(point[1] / 2); - } - }; - - List points = new ArrayList<>(); - for (int i = 0; i < numPts; i++) { - for (int j = 0; j < numPts; j++) { - double[] point = {x[i][j], y[i][j]}; - f[i][j] = func.value(point); - points.add(new Vector3D(x[i][j], y[i][j], f[i][j])); - } - } - - FitSurface app = new FitSurface(points, degree); - - MultivariateFunction fit = new MultivariateFunction() { - @Override - public double value(double[] point) { - return app.value(point[0], point[1]); - } - }; - - - List surfaceFit = new ArrayList<>(); - for (int i = 0; i < numPts; i++) { - for (int j = 0; j < numPts; j++) { - double[] point = {x[i][j], y[i][j]}; - f[i][j] = fit.value(point); - surfaceFit.add(new Vector3D(x[i][j], y[i][j], f[i][j])); - } - } - - MultivariateFunction diff = new MultivariateFunction() { - @Override - public double value(double[] point) { - return func.value(point) - fit.value(point); - } - }; - - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).title("Func").build(); - - AxisX xLowerAxis = new AxisX(0, maxX, "X (degrees)", StringFunctions.toDegrees("%.0f")); - AxisY yLeftAxis = new AxisY(0, maxX, "Y (degrees)", StringFunctions.toDegrees("%.0f")); - - ColorRamp ramp = ColorRamp.create(TYPE.PARULA, -1, 1); - - AreaPlot canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); - canvas.plot(func, ramp, xLowerAxis, yLeftAxis); - // Vertical, along right side - canvas.drawColorBar(ImmutableColorBar.builder() - .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) - .ramp(ramp).numTicks(9).tickFunction(StringFunctions.fixedFormat("%.2f")).build()); - - PlotCanvas.showJFrame(canvas.getImage()); - - config = ImmutablePlotConfig.copyOf(config).withTitle("Fit"); - canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); - canvas.plot(fit, ramp, xLowerAxis, yLeftAxis); - // Vertical, along right side - canvas.drawColorBar(ImmutableColorBar.builder() - .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) - .ramp(ramp).numTicks(9).tickFunction(StringFunctions.fixedFormat("%.2f")).build()); - - PlotCanvas.showJFrame(canvas.getImage()); - - config = ImmutablePlotConfig.copyOf(config).withTitle("Func-Fit"); - ramp = ColorRamp.create(TYPE.PARULA, -1, 1); - ramp.addLimitColors(); - canvas = new AreaPlot(config); - canvas.setAxes(xLowerAxis, yLeftAxis); - canvas.drawAxes(); - canvas.plot(diff, ramp, xLowerAxis, yLeftAxis); - // Vertical, along right side - canvas.drawColorBar(ImmutableColorBar.builder() - .rect(new Rectangle(canvas.getPageWidth() - 60, config.topMargin(), 10, config.height())) - .ramp(ramp).numTicks(9).tickFunction(StringFunctions.fixedFormat("%.2f")).build()); - - PlotCanvas.showJFrame(canvas.getImage()); - - PolyDataUtil.writeVTKPoints("points.vtk", points); - PolyDataUtil.writeVTKPoints("surface" + degree + ".vtk", surfaceFit); - } - } diff --git a/src/test/java/terrasaur/utils/RemoveAberrationTest.java b/src/test/java/terrasaur/utils/RemoveAberrationTest.java index 0003866..e4701fd 100644 --- a/src/test/java/terrasaur/utils/RemoveAberrationTest.java +++ b/src/test/java/terrasaur/utils/RemoveAberrationTest.java @@ -23,6 +23,7 @@ package terrasaur.utils; import static org.junit.Assert.assertEquals; + import java.io.File; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -42,107 +43,138 @@ import spice.basic.Vector3; public class RemoveAberrationTest { - private static final Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(); - @BeforeClass - public static void setup() throws SpiceErrorException { - NativeLibraryLoader.loadSpiceLibraries(); + @BeforeClass + public static void setup() throws SpiceErrorException { + NativeLibraryLoader.loadSpiceLibraries(); - File lsk = ResourceUtils.writeResourceToFile("/resources/kernels/lsk/naif0012.tls"); - File spk = ResourceUtils.writeResourceToFile("/resources/kernels/spk/de432s.bsp"); + File lsk = ResourceUtils.writeResourceToFile("/resources/kernels/lsk/naif0012.tls"); + File spk = ResourceUtils.writeResourceToFile("/resources/kernels/spk/de432s.bsp"); - if (!lsk.exists()) { - logger.error("LSK file {} does not exist!", lsk.getAbsolutePath()); - } - if (!spk.exists()) { - logger.error("SPK file {} does not exist!", spk.getAbsolutePath()); + if (!lsk.exists()) { + logger.error("LSK file {} does not exist!", lsk.getAbsolutePath()); + } + if (!spk.exists()) { + logger.error("SPK file {} does not exist!", spk.getAbsolutePath()); + } + + KernelDatabase.load(lsk.getAbsolutePath()); + KernelDatabase.load(spk.getAbsolutePath()); + + Log4j2Configurator.getInstance(); } - KernelDatabase.load(lsk.getAbsolutePath()); - KernelDatabase.load(spk.getAbsolutePath()); + @Test + public void test01() throws SpiceException { - Log4j2Configurator.getInstance(); - } + // https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/spicelib/stelab.html + // light time corrected vector + double[] pos = {201738.725087, -260893.141602, -147722.589056}; + Vector3 posV = new Vector3(pos); - @Test - public void test01() throws SpiceException { + // observer velocity wrt SSB + double[] vel = {28.611751, 5.7275129, 2.4830453}; + Vector3 velV = new Vector3(vel); - // https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/spicelib/stelab.html + double[] corrected = {201765.929516, -260876.818077, -147714.262441}; + Vector3 correctedV = RemoveAberration.stelab(posV, velV); - // light time corrected vector - double[] pos = {201738.725087, -260893.141602, -147722.589056}; - Vector3 posV = new Vector3(pos); + assertEquals(correctedV.sep(new Vector3(corrected)), 0, 1e-12); + } - // observer velocity wrt SSB - double[] vel = {28.611751, 5.7275129, 2.4830453}; - Vector3 velV = new Vector3(vel); + @Test + public void test02() throws SpiceException { - double[] corrected = {201765.929516, -260876.818077, -147714.262441}; - Vector3 correctedV = RemoveAberration.stelab(posV, velV); + Body target = new Body("MOON"); + Body obs = new Body("EARTH"); + Body ssb = new Body(0); + TDBTime et = new TDBTime("July 4 2004"); + ReferenceFrame j2000 = new ReferenceFrame("J2000"); + StateRecord sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT"), obs); - assertEquals(correctedV.sep(new Vector3(corrected)), 0, 1e-12); - } + Vector3 pos = sr.getPosition(); - @Test - public void test02() throws SpiceException { + sr = new StateRecord(obs, et, j2000, new AberrationCorrection("NONE"), ssb); + Vector3 vel = sr.getVelocity(); - Body target = new Body("MOON"); - Body obs = new Body("EARTH"); - Body ssb = new Body(0); - TDBTime et = new TDBTime("July 4 2004"); - ReferenceFrame j2000 = new ReferenceFrame("J2000"); - StateRecord sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT"), obs); + Vector3 corrected = RemoveAberration.stelab(pos, vel); - Vector3 pos = sr.getPosition(); + logger.printf( + Level.DEBUG, + "Uncorrected position vector (LT): %f %f %f", + pos.getElt(0), + pos.getElt(1), + pos.getElt(2)); + logger.printf( + Level.DEBUG, + "Velocity vector: %f %f %f", + vel.getElt(0), + vel.getElt(1), + vel.getElt(2)); + logger.printf( + Level.DEBUG, + "Corrected position vector: %f %f %f", + corrected.getElt(0), + corrected.getElt(1), + corrected.getElt(2)); - sr = new StateRecord(obs, et, j2000, new AberrationCorrection("NONE"), ssb); - Vector3 vel = sr.getVelocity(); + corrected = new Vector3(CSPICE.stelab(pos.toArray(), vel.toArray())); + logger.printf( + Level.DEBUG, + "Corrected position vector (stelab): %f %f %f", + corrected.getElt(0), + corrected.getElt(1), + corrected.getElt(2)); - Vector3 corrected = RemoveAberration.stelab(pos, vel); + sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT+S"), obs); + corrected = sr.getPosition(); + logger.printf( + Level.DEBUG, + "Corrected position vector (LT+S): %f %f %f", + corrected.getElt(0), + corrected.getElt(1), + corrected.getElt(2)); - logger.printf(Level.DEBUG, "Uncorrected position vector (LT): %f %f %f", pos.getElt(0), - pos.getElt(1), pos.getElt(2)); - logger.printf(Level.DEBUG, "Velocity vector: %f %f %f", vel.getElt(0), - vel.getElt(1), vel.getElt(2)); - logger.printf(Level.DEBUG, "Corrected position vector: %f %f %f", corrected.getElt(0), - corrected.getElt(1), corrected.getElt(2)); + assertEquals(RemoveAberration.stelab(pos, vel).sep(corrected), 0, 1e-12); + } - corrected = new Vector3(CSPICE.stelab(pos.toArray(), vel.toArray())); - logger.printf(Level.DEBUG, "Corrected position vector (stelab): %f %f %f", corrected.getElt(0), - corrected.getElt(1), corrected.getElt(2)); + @Test + public void test03() throws SpiceException { + Body target = new Body("MOON"); + Body obs = new Body("EARTH"); + TDBTime et = new TDBTime("July 4 2004"); + ReferenceFrame j2000 = new ReferenceFrame("J2000"); - sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT+S"), obs); - corrected = sr.getPosition(); - logger.printf(Level.DEBUG, "Corrected position vector (LT+S): %f %f %f", corrected.getElt(0), - corrected.getElt(1), corrected.getElt(2)); + RemoveAberration ra = new RemoveAberration(target, obs); - assertEquals(RemoveAberration.stelab(pos, vel).sep(corrected), 0, 1e-12); - } + StateRecord sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT+S"), obs); + Vector3 corrected = sr.getPosition(); + logger.printf( + Level.DEBUG, + "Corrected position vector (LT+S): %f %f %f", + corrected.getElt(0), + corrected.getElt(1), + corrected.getElt(2)); - @Test - public void test03() throws SpiceException { - Body target = new Body("MOON"); - Body obs = new Body("EARTH"); - TDBTime et = new TDBTime("July 4 2004"); - ReferenceFrame j2000 = new ReferenceFrame("J2000"); + sr = new StateRecord(target, et, j2000, new AberrationCorrection("NONE"), obs); + Vector3 uncorrected = sr.getPosition(); + logger.printf( + Level.DEBUG, + "Uncorrected position vector (NONE): %f %f %f", + uncorrected.getElt(0), + uncorrected.getElt(1), + uncorrected.getElt(2)); - RemoveAberration ra = new RemoveAberration(target, obs); + uncorrected = ra.getGeometricPosition(et, corrected); + logger.printf( + Level.DEBUG, + "Uncorrected position vector (Estimated): %f %f %f", + uncorrected.getElt(0), + uncorrected.getElt(1), + uncorrected.getElt(2)); - StateRecord sr = new StateRecord(target, et, j2000, new AberrationCorrection("LT+S"), obs); - Vector3 corrected = sr.getPosition(); - logger.printf(Level.DEBUG, "Corrected position vector (LT+S): %f %f %f", - corrected.getElt(0), corrected.getElt(1), corrected.getElt(2)); - - sr = new StateRecord(target, et, j2000, new AberrationCorrection("NONE"), obs); - Vector3 uncorrected = sr.getPosition(); - logger.printf(Level.DEBUG, "Uncorrected position vector (NONE): %f %f %f", - uncorrected.getElt(0), uncorrected.getElt(1), uncorrected.getElt(2)); - - uncorrected = ra.getGeometricPosition(et, corrected); - logger.printf(Level.DEBUG, "Uncorrected position vector (Estimated): %f %f %f", - uncorrected.getElt(0), uncorrected.getElt(1), uncorrected.getElt(2)); - - assertEquals(sr.getPosition().sep(uncorrected), 0, 1e-8); - } + assertEquals(sr.getPosition().sep(uncorrected), 0, 1e-8); + } } diff --git a/src/test/java/terrasaur/utils/ResourceUtilsTest.java b/src/test/java/terrasaur/utils/ResourceUtilsTest.java index 9e769d7..2e69d90 100644 --- a/src/test/java/terrasaur/utils/ResourceUtilsTest.java +++ b/src/test/java/terrasaur/utils/ResourceUtilsTest.java @@ -23,37 +23,37 @@ package terrasaur.utils; import static org.junit.Assert.assertEquals; + +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.common.io.Resources; import java.io.File; import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.FileUtils; import org.junit.Test; -import com.google.common.hash.HashCode; -import com.google.common.hash.Hashing; -import com.google.common.io.ByteSource; -import com.google.common.io.Resources; public class ResourceUtilsTest { - @Test - public void testWriteResourceToFile() throws IOException { - String path = "/resources/kernels/lsk/naif0012.tls"; + @Test + public void testWriteResourceToFile() throws IOException { + String path = "/resources/kernels/lsk/naif0012.tls"; - ByteSource byteSrc = Resources.asByteSource(ResourceUtilsTest.class.getResource(path)); - InputStream is; + ByteSource byteSrc = Resources.asByteSource(ResourceUtilsTest.class.getResource(path)); + InputStream is; - is = byteSrc.openBufferedStream(); + is = byteSrc.openBufferedStream(); - byte[] buffer = new byte[is.available()]; - HashCode resource = Hashing.sha256().hashBytes(buffer); + byte[] buffer = new byte[is.available()]; + HashCode resource = Hashing.sha256().hashBytes(buffer); - File lsk = ResourceUtils.writeResourceToFile(path); + File lsk = ResourceUtils.writeResourceToFile(path); - is = FileUtils.openInputStream(lsk); - buffer = new byte[is.available()]; - HashCode copy = Hashing.sha256().hashBytes(buffer); - - assertEquals(resource, copy); - } + is = FileUtils.openInputStream(lsk); + buffer = new byte[is.available()]; + HashCode copy = Hashing.sha256().hashBytes(buffer); + assertEquals(resource, copy); + } } diff --git a/src/test/java/terrasaur/utils/SumFileTest.java b/src/test/java/terrasaur/utils/SumFileTest.java index 400d434..7d89af5 100644 --- a/src/test/java/terrasaur/utils/SumFileTest.java +++ b/src/test/java/terrasaur/utils/SumFileTest.java @@ -40,68 +40,60 @@ import terrasaur.utils.spice.SpiceBundle; public class SumFileTest { - @Test - public void testFrustum() throws IOException { + @Test + public void testFrustum() throws IOException { - File file = ResourceUtils.writeResourceToFile("/M605862153F5.SUM"); + File file = ResourceUtils.writeResourceToFile("/M605862153F5.SUM"); - Assert.assertNotNull(file); - List lines = FileUtils.readLines(file, Charset.defaultCharset()); + Assert.assertNotNull(file); + List lines = FileUtils.readLines(file, Charset.defaultCharset()); - SumFile sumFile = SumFile.fromLines(lines); + SumFile sumFile = SumFile.fromLines(lines); - 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); - } + 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( + @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); + 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"); + double t = bundle.getTimeConversion().utcStringToTDB("2022 SEP 26 23:14:23.328"); - SumFile sumFile2 = - SumFile.fromFile( - new File( + 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); + SumFile sumFile3 = sumFile1.fromSpice(bundle, observer, target, fov.getFrameID(), t); - System.out.println(sumFile1); - System.out.println(sumFile2); - System.out.println(sumFile3); - } + System.out.println(sumFile1); + System.out.println(sumFile2); + System.out.println(sumFile3); + } } diff --git a/src/test/java/terrasaur/utils/batch/TestBatchSubmit.java b/src/test/java/terrasaur/utils/batch/TestBatchSubmit.java index 8a7c6ca..c2a13ac 100644 --- a/src/test/java/terrasaur/utils/batch/TestBatchSubmit.java +++ b/src/test/java/terrasaur/utils/batch/TestBatchSubmit.java @@ -38,48 +38,44 @@ import org.junit.Test; public class TestBatchSubmit { - @Before - public void setup() { - // set logger configuration + @Before + public void setup() { + // set logger configuration - // see https://www.baeldung.com/log4j2-programmatic-config and - // https://logging.apache.org/log4j/2.x/manual/customconfig.html - ConfigurationBuilder builder = - ConfigurationBuilderFactory.newConfigurationBuilder(); + // see https://www.baeldung.com/log4j2-programmatic-config and + // https://logging.apache.org/log4j/2.x/manual/customconfig.html + ConfigurationBuilder builder = ConfigurationBuilderFactory.newConfigurationBuilder(); - AppenderComponentBuilder console = builder.newAppender("Stdout", "CONSOLE"); + AppenderComponentBuilder console = builder.newAppender("Stdout", "CONSOLE"); - LayoutComponentBuilder layout = builder.newLayout("PatternLayout"); - layout.addAttribute("pattern", "%d %-5level [%C{1} %M:%L] %msg%n%throwable"); - console.add(layout); + LayoutComponentBuilder layout = builder.newLayout("PatternLayout"); + layout.addAttribute("pattern", "%d %-5level [%C{1} %M:%L] %msg%n%throwable"); + console.add(layout); - builder.add(console); + builder.add(console); - RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.DEBUG); - rootLogger.add(builder.newAppenderRef("Stdout")); - builder.add(rootLogger); + RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.DEBUG); + rootLogger.add(builder.newAppenderRef("Stdout")); + builder.add(rootLogger); - Configurator.initialize(builder.build()); - } - - @Test - public void testLocalSequential() { - BatchType batchType = BatchType.LOCAL_SEQUENTIAL; - GridType gridType = GridType.LOCAL; - - List commandList = new ArrayList<>(); - - commandList.add("uname -a"); - - BatchSubmitI batchSubmit = BatchSubmitFactory.getBatchSubmit(commandList, batchType, gridType); - - try { - batchSubmit.runBatchSubmitinDir("."); - } catch (InterruptedException | IOException e) { - e.printStackTrace(); + Configurator.initialize(builder.build()); } - } + @Test + public void testLocalSequential() { + BatchType batchType = BatchType.LOCAL_SEQUENTIAL; + GridType gridType = GridType.LOCAL; + List commandList = new ArrayList<>(); + commandList.add("uname -a"); + + BatchSubmitI batchSubmit = BatchSubmitFactory.getBatchSubmit(commandList, batchType, gridType); + + try { + batchSubmit.runBatchSubmitinDir("."); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + } + } } diff --git a/src/test/java/terrasaur/utils/lidar/LidarTransformationTest.java b/src/test/java/terrasaur/utils/lidar/LidarTransformationTest.java index bda548d..6cdd24d 100644 --- a/src/test/java/terrasaur/utils/lidar/LidarTransformationTest.java +++ b/src/test/java/terrasaur/utils/lidar/LidarTransformationTest.java @@ -23,6 +23,7 @@ package terrasaur.utils.lidar; import static org.junit.Assert.assertTrue; + import java.io.File; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import org.junit.BeforeClass; @@ -31,44 +32,40 @@ import terrasaur.utils.ResourceUtils; public class LidarTransformationTest { - private static LidarTransformation testTransform; + private static LidarTransformation testTransform; - @BeforeClass - public static void setup() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - sb.append( - "\"translation\" : [ -2.7063546673442025e-06 , -3.2956184403440832e-06 , -3.1679187973381386e-06 ],"); - sb.append( - "\"rotation\" : [ 9.9999990651910198e-01 , -3.6506246582060722e-04 , 1.7564641331158977e-04 , -1.5112749854652996e-04 ],"); - sb.append( - "\"centerOfRotation\" : [ -2.1905955000000015e-02 , -7.5626499999999685e-04 , -1.4996199999999572e-03 ],"); - sb.append("\"startId\" : 0,"); - sb.append("\"stopId\" : 0,"); - sb.append("\"minErrorBefore\" : 9.2036295980522610e-14,"); - sb.append("\"maxErrorBefore\" : 4.6214268134231436e-03,"); - sb.append("\"rmsBefore\" : 4.9683609628854209e-04,"); - sb.append("\"meanErrorBefore\" : 3.1790339970386226e-04,"); - sb.append("\"stdBefore\" : 3.8181610106432639e-04,"); - sb.append("\"minErrorAfter\" : 5.3934126541711475e-09,"); - sb.append("\"maxErrorAfter\" : 4.6591641396458482e-03,"); - sb.append("\"rmsAfter\" : 4.9685848009884737e-04,"); - sb.append("\"meanErrorAfter\" : 3.2673464939622047e-04,"); - sb.append("\"stdAfter\" : 3.7431646788521823e-04"); - sb.append("}"); - - testTransform = LidarTransformation.fromJSON(sb.toString()); - } - - @Test - public void testReader() { - String path = "/lidar/transformationfile.txt"; - File file = ResourceUtils.writeResourceToFile(path); - LidarTransformation transform = LidarTransformation.fromJSON(file); - - assertTrue(Vector3D.angle(transform.getTranslation(), testTransform.getTranslation()) < 1e-5); - } + @BeforeClass + public static void setup() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"translation\" : [ -2.7063546673442025e-06 , -3.2956184403440832e-06 , -3.1679187973381386e-06 ],"); + sb.append( + "\"rotation\" : [ 9.9999990651910198e-01 , -3.6506246582060722e-04 , 1.7564641331158977e-04 , -1.5112749854652996e-04 ],"); + sb.append( + "\"centerOfRotation\" : [ -2.1905955000000015e-02 , -7.5626499999999685e-04 , -1.4996199999999572e-03 ],"); + sb.append("\"startId\" : 0,"); + sb.append("\"stopId\" : 0,"); + sb.append("\"minErrorBefore\" : 9.2036295980522610e-14,"); + sb.append("\"maxErrorBefore\" : 4.6214268134231436e-03,"); + sb.append("\"rmsBefore\" : 4.9683609628854209e-04,"); + sb.append("\"meanErrorBefore\" : 3.1790339970386226e-04,"); + sb.append("\"stdBefore\" : 3.8181610106432639e-04,"); + sb.append("\"minErrorAfter\" : 5.3934126541711475e-09,"); + sb.append("\"maxErrorAfter\" : 4.6591641396458482e-03,"); + sb.append("\"rmsAfter\" : 4.9685848009884737e-04,"); + sb.append("\"meanErrorAfter\" : 3.2673464939622047e-04,"); + sb.append("\"stdAfter\" : 3.7431646788521823e-04"); + sb.append("}"); + testTransform = LidarTransformation.fromJSON(sb.toString()); + } + @Test + public void testReader() { + String path = "/lidar/transformationfile.txt"; + File file = ResourceUtils.writeResourceToFile(path); + LidarTransformation transform = LidarTransformation.fromJSON(file); + assertTrue(Vector3D.angle(transform.getTranslation(), testTransform.getTranslation()) < 1e-5); + } } diff --git a/src/test/java/terrasaur/utils/math/MathConversionsTest.java b/src/test/java/terrasaur/utils/math/MathConversionsTest.java index 84f8f97..ab3150d 100644 --- a/src/test/java/terrasaur/utils/math/MathConversionsTest.java +++ b/src/test/java/terrasaur/utils/math/MathConversionsTest.java @@ -23,6 +23,7 @@ package terrasaur.utils.math; import static org.junit.Assert.assertTrue; + import java.util.Random; import org.apache.commons.math3.complex.Quaternion; import org.apache.commons.math3.geometry.euclidean.threed.Rotation; @@ -32,267 +33,259 @@ import picante.math.vectorspace.RotationMatrixIJK; import picante.math.vectorspace.UnwritableRotationMatrixIJK; import picante.math.vectorspace.VectorIJK; import picante.mechanics.rotations.AxisAndAngle; -import terrasaur.utils.NativeLibraryLoader; -import terrasaur.utils.VectorUtils; import spice.basic.Matrix33; import spice.basic.SpiceException; import spice.basic.Vector3; +import terrasaur.utils.NativeLibraryLoader; +import terrasaur.utils.VectorUtils; public class MathConversionsTest { - private boolean compareRotations(Rotation a, Rotation b) { - Quaternion qa = - new Quaternion(a.getQ0(), a.getQ1(), a.getQ2(), a.getQ3()).getPositivePolarForm(); - Quaternion qb = - new Quaternion(b.getQ0(), b.getQ1(), b.getQ2(), b.getQ3()).getPositivePolarForm(); + private boolean compareRotations(Rotation a, Rotation b) { + Quaternion qa = new Quaternion(a.getQ0(), a.getQ1(), a.getQ2(), a.getQ3()).getPositivePolarForm(); + Quaternion qb = new Quaternion(b.getQ0(), b.getQ1(), b.getQ2(), b.getQ3()).getPositivePolarForm(); - return qa.equals(qb, 1e-6); - } + return qa.equals(qb, 1e-6); + } - private boolean compareRotations(UnwritableRotationMatrixIJK a, UnwritableRotationMatrixIJK b) { + private boolean compareRotations(UnwritableRotationMatrixIJK a, UnwritableRotationMatrixIJK b) { - RotationMatrixIJK identity = RotationMatrixIJK.mtxm(a, b); + RotationMatrixIJK identity = RotationMatrixIJK.mtxm(a, b); - AxisAndAngle aaa = new AxisAndAngle(identity); + AxisAndAngle aaa = new AxisAndAngle(identity); - return aaa.getAngle() < 1e-6; - } + return aaa.getAngle() < 1e-6; + } - private boolean compareRotations(Matrix33 a, Matrix33 b) throws SpiceException { + private boolean compareRotations(Matrix33 a, Matrix33 b) throws SpiceException { - Matrix33 identity = a.mtxm(b); + Matrix33 identity = a.mtxm(b); - spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(identity); + spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(identity); - return aaa.getAngle() < 1e-6; - } + return aaa.getAngle() < 1e-6; + } - /** - * Test Apache -> Picante -> Apache results in identical rotations. - */ - @Test - public void testACA() { + /** + * Test Apache -> Picante -> Apache results in identical rotations. + */ + @Test + public void testACA() { - Rotation rInitial = RotationUtils.randomRotation(); - RotationMatrixIJK rPicante = MathConversions.toRotationMatrixIJK(rInitial); - Rotation rFinal = MathConversions.toRotation(rPicante); + Rotation rInitial = RotationUtils.randomRotation(); + RotationMatrixIJK rPicante = MathConversions.toRotationMatrixIJK(rInitial); + Rotation rFinal = MathConversions.toRotation(rPicante); - /*- - System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), - rInitial.getQ3()); - System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), - rFinal.getQ3()); + /*- + System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), + rInitial.getQ3()); + System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), + rFinal.getQ3()); + */ + + assertTrue(compareRotations(rInitial, rFinal)); + } + + /** + * Test Apache -> SPICE -> Apache results in identical rotations. + */ + @Test + public void testASA() { + + NativeLibraryLoader.loadSpiceLibraries(); + + Rotation rInitial = RotationUtils.randomRotation(); + Matrix33 rSpice = MathConversions.toMatrix33(rInitial); + Rotation rFinal = MathConversions.toRotation(rSpice); + + /*- + System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), + rInitial.getQ3()); + System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), + rFinal.getQ3()); */ - assertTrue(compareRotations(rInitial, rFinal)); - } + assertTrue(compareRotations(rInitial, rFinal)); + } - /** - * Test Apache -> SPICE -> Apache results in identical rotations. - */ - @Test - public void testASA() { + /** + * Test Apache -> Picante translation results in identical frame transformations + */ + @Test + public void testAC() { + Vector3D iRow = VectorUtils.randomVector(); + Vector3D jRow = VectorUtils.randomVector(); - NativeLibraryLoader.loadSpiceLibraries(); + Rotation mApache = RotationUtils.IprimaryJsecondary(iRow, jRow); + RotationMatrixIJK mPicante = MathConversions.toRotationMatrixIJK(mApache); - Rotation rInitial = RotationUtils.randomRotation(); - Matrix33 rSpice = MathConversions.toMatrix33(rInitial); - Rotation rFinal = MathConversions.toRotation(rSpice); + Vector3D vApache = VectorUtils.randomVector(); + VectorIJK vPicante = MathConversions.toVectorIJK(vApache); - /*- - System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), - rInitial.getQ3()); - System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), - rFinal.getQ3()); - */ + Vector3D vRotated = mApache.applyTo(vApache); + vPicante = mPicante.mxv(vPicante); - assertTrue(compareRotations(rInitial, rFinal)); - } + /*- + System.out.println(vRotated); + System.out.println(vPicante); + */ + assertTrue(Vector3D.angle(vRotated, MathConversions.toVector3D(vPicante)) < 1e-6); + } - /** - * Test Apache -> Picante translation results in identical frame transformations - */ - @Test - public void testAC() { - Vector3D iRow = VectorUtils.randomVector(); - Vector3D jRow = VectorUtils.randomVector(); + /** + * Test Apache -> SPICE translation results in identical frame transformations + */ + @Test + public void testAS() { + NativeLibraryLoader.loadSpiceLibraries(); - Rotation mApache = RotationUtils.IprimaryJsecondary(iRow, jRow); - RotationMatrixIJK mPicante = MathConversions.toRotationMatrixIJK(mApache); + Vector3D iRow = VectorUtils.randomVector(); + Vector3D jRow = VectorUtils.randomVector(); - Vector3D vApache = VectorUtils.randomVector(); - VectorIJK vPicante = MathConversions.toVectorIJK(vApache); + Rotation mApache = RotationUtils.IprimaryJsecondary(iRow, jRow); + Matrix33 mSpice = MathConversions.toMatrix33(mApache); - Vector3D vRotated = mApache.applyTo(vApache); - vPicante = mPicante.mxv(vPicante); + Vector3D vApache = VectorUtils.randomVector(); + Vector3 vSpice = MathConversions.toVector3(vApache); - /*- - System.out.println(vRotated); - System.out.println(vPicante); - */ + Vector3D vRotated = mApache.applyTo(vApache); + vSpice = mSpice.mxv(vSpice); - assertTrue(Vector3D.angle(vRotated, MathConversions.toVector3D(vPicante)) < 1e-6); - } + /*- + System.out.println(vRotated); + System.out.println(vSpice); + */ - /** - * Test Apache -> SPICE translation results in identical frame transformations - */ - @Test - public void testAS() { - NativeLibraryLoader.loadSpiceLibraries(); + assertTrue(Vector3D.angle(vRotated, MathConversions.toVector3D(vSpice)) < 1e-6); + } - Vector3D iRow = VectorUtils.randomVector(); - Vector3D jRow = VectorUtils.randomVector(); + /** + * Test Picante -> Apache translation results in identical frame transformations + */ + @Test + public void testCA() { + VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); + double angle = new Random().nextDouble() * 2 * Math.PI; + AxisAndAngle aaa = new AxisAndAngle(axis, angle); - Rotation mApache = RotationUtils.IprimaryJsecondary(iRow, jRow); - Matrix33 mSpice = MathConversions.toMatrix33(mApache); + RotationMatrixIJK mPicante = aaa.getRotation(new RotationMatrixIJK()); + Rotation mApache = MathConversions.toRotation(mPicante); - Vector3D vApache = VectorUtils.randomVector(); - Vector3 vSpice = MathConversions.toVector3(vApache); + Vector3D vApache = VectorUtils.randomVector(); + VectorIJK vPicante = MathConversions.toVectorIJK(vApache); - Vector3D vRotated = mApache.applyTo(vApache); - vSpice = mSpice.mxv(vSpice); + vApache = mApache.applyTo(vApache); + vPicante = mPicante.mxv(vPicante); + /*- + System.out.println(vApache); + System.out.println(vPicante); + */ + assertTrue(Vector3D.angle(vApache, MathConversions.toVector3D(vPicante)) < 1e-6); + } - /*- - System.out.println(vRotated); - System.out.println(vSpice); - */ + /** + * Test Picante -> Apache -> Picante results in identical rotations. + */ + @Test + public void testCAC() { - assertTrue(Vector3D.angle(vRotated, MathConversions.toVector3D(vSpice)) < 1e-6); - } + VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); + double angle = new Random().nextDouble() * 2 * Math.PI; + AxisAndAngle aaa = new AxisAndAngle(axis, angle); - /** - * Test Picante -> Apache translation results in identical frame transformations - */ - @Test - public void testCA() { - VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); - double angle = new Random().nextDouble() * 2 * Math.PI; - AxisAndAngle aaa = new AxisAndAngle(axis, angle); + UnwritableRotationMatrixIJK rInitial = aaa.getRotation(new RotationMatrixIJK()); + Rotation rApache = MathConversions.toRotation(rInitial); + UnwritableRotationMatrixIJK rFinal = MathConversions.toRotationMatrixIJK(rApache); - RotationMatrixIJK mPicante = aaa.getRotation(new RotationMatrixIJK()); - Rotation mApache = MathConversions.toRotation(mPicante); + /*- + System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), + rInitial.getQ3()); + System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), + rFinal.getQ3()); + */ - Vector3D vApache = VectorUtils.randomVector(); - VectorIJK vPicante = MathConversions.toVectorIJK(vApache); + assertTrue(compareRotations(rInitial, rFinal)); + } - vApache = mApache.applyTo(vApache); - vPicante = mPicante.mxv(vPicante); - /*- - System.out.println(vApache); - System.out.println(vPicante); - */ - assertTrue(Vector3D.angle(vApache, MathConversions.toVector3D(vPicante)) < 1e-6); - } + /** + * Test Picante -> SPICE translation results in identical frame transformations + */ + @Test + public void testCS() { + NativeLibraryLoader.loadSpiceLibraries(); - /** - * Test Picante -> Apache -> Picante results in identical rotations. - */ - @Test - public void testCAC() { + VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); + double angle = new Random().nextDouble() * 2 * Math.PI; + AxisAndAngle aaa = new AxisAndAngle(axis, angle); - VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); - double angle = new Random().nextDouble() * 2 * Math.PI; - AxisAndAngle aaa = new AxisAndAngle(axis, angle); + RotationMatrixIJK mPicante = aaa.getRotation(new RotationMatrixIJK()); + Matrix33 mSpice = MathConversions.toMatrix33(mPicante); - UnwritableRotationMatrixIJK rInitial = aaa.getRotation(new RotationMatrixIJK()); - Rotation rApache = MathConversions.toRotation(rInitial); - UnwritableRotationMatrixIJK rFinal = MathConversions.toRotationMatrixIJK(rApache); + VectorIJK vPicante = MathConversions.toVectorIJK(VectorUtils.randomVector()); + Vector3 vSpice = MathConversions.toVector3(vPicante); - /*- - System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), - rInitial.getQ3()); - System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), - rFinal.getQ3()); - */ + vSpice = mSpice.mxv(vSpice); + vPicante = mPicante.mxv(vPicante); + /*- + System.out.println(vSpice); + System.out.println(vPicante); + */ + assertTrue(vSpice.sep(MathConversions.toVector3(vPicante)) < 1e-6); + } - assertTrue(compareRotations(rInitial, rFinal)); - } + /** + * Test SPICE -> Apache translation results in identical frame transformations + */ + @Test + public void testSA() throws SpiceException { + NativeLibraryLoader.loadSpiceLibraries(); - /** - * Test Picante -> SPICE translation results in identical frame transformations - */ - @Test - public void testCS() { - NativeLibraryLoader.loadSpiceLibraries(); + Vector3 axis = MathConversions.toVector3(VectorUtils.randomVector()); + double angle = new Random().nextDouble() * 2 * Math.PI; + spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(axis, angle); - VectorIJK axis = MathConversions.toVectorIJK(VectorUtils.randomVector()); - double angle = new Random().nextDouble() * 2 * Math.PI; - AxisAndAngle aaa = new AxisAndAngle(axis, angle); + Matrix33 mSpice = aaa.toMatrix(); + Rotation mApache = MathConversions.toRotation(mSpice); - RotationMatrixIJK mPicante = aaa.getRotation(new RotationMatrixIJK()); - Matrix33 mSpice = MathConversions.toMatrix33(mPicante); + Vector3D vApache = VectorUtils.randomVector(); + Vector3 vSpice = MathConversions.toVector3(vApache); - VectorIJK vPicante = MathConversions.toVectorIJK(VectorUtils.randomVector()); - Vector3 vSpice = MathConversions.toVector3(vPicante); + vApache = mApache.applyTo(vApache); + vSpice = mSpice.mxv(vSpice); + /*- + System.out.println(vApache); + System.out.println(vSpice); + */ + assertTrue(Vector3D.angle(vApache, MathConversions.toVector3D(vSpice)) < 1e-6); + } - vSpice = mSpice.mxv(vSpice); - vPicante = mPicante.mxv(vPicante); - /*- - System.out.println(vSpice); - System.out.println(vPicante); - */ - assertTrue(vSpice.sep(MathConversions.toVector3(vPicante)) < 1e-6); - } + /** + * Test SPICE -> Apache -> SPICE results in identical rotations. + * + * @throws SpiceException + */ + @Test + public void testSAS() throws SpiceException { - /** - * Test SPICE -> Apache translation results in identical frame transformations - */ - @Test - public void testSA() throws SpiceException { + NativeLibraryLoader.loadSpiceLibraries(); - NativeLibraryLoader.loadSpiceLibraries(); - - Vector3 axis = MathConversions.toVector3(VectorUtils.randomVector()); - double angle = new Random().nextDouble() * 2 * Math.PI; - spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(axis, angle); - - Matrix33 mSpice = aaa.toMatrix(); - Rotation mApache = MathConversions.toRotation(mSpice); - - Vector3D vApache = VectorUtils.randomVector(); - Vector3 vSpice = MathConversions.toVector3(vApache); - - vApache = mApache.applyTo(vApache); - vSpice = mSpice.mxv(vSpice); - /*- - System.out.println(vApache); - System.out.println(vSpice); - */ - assertTrue(Vector3D.angle(vApache, MathConversions.toVector3D(vSpice)) < 1e-6); - - } - - /** - * Test SPICE -> Apache -> SPICE results in identical rotations. - * - * @throws SpiceException - */ - @Test - public void testSAS() throws SpiceException { - - NativeLibraryLoader.loadSpiceLibraries(); - - Vector3 axis = MathConversions.toVector3(VectorUtils.randomVector()); - double angle = new Random().nextDouble() * 2 * Math.PI; - spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(axis, angle); - - Matrix33 rInitial = aaa.toMatrix(); - Rotation rApache = MathConversions.toRotation(rInitial); - Matrix33 rFinal = MathConversions.toMatrix33(rApache); - - /*- - System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), - rInitial.getQ3()); - System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), - rFinal.getQ3()); - */ - - assertTrue(compareRotations(rInitial, rFinal)); - } + Vector3 axis = MathConversions.toVector3(VectorUtils.randomVector()); + double angle = new Random().nextDouble() * 2 * Math.PI; + spice.basic.AxisAndAngle aaa = new spice.basic.AxisAndAngle(axis, angle); + Matrix33 rInitial = aaa.toMatrix(); + Rotation rApache = MathConversions.toRotation(rInitial); + Matrix33 rFinal = MathConversions.toMatrix33(rApache); + /*- + System.out.printf("%f %f %f %f\n", rInitial.getQ0(), rInitial.getQ1(), rInitial.getQ2(), + rInitial.getQ3()); + System.out.printf("%f %f %f %f\n", rFinal.getQ0(), rFinal.getQ1(), rFinal.getQ2(), + rFinal.getQ3()); + */ + assertTrue(compareRotations(rInitial, rFinal)); + } } diff --git a/src/test/java/terrasaur/utils/math/RotationUtilsTest.java b/src/test/java/terrasaur/utils/math/RotationUtilsTest.java index 9e90083..6b6648c 100644 --- a/src/test/java/terrasaur/utils/math/RotationUtilsTest.java +++ b/src/test/java/terrasaur/utils/math/RotationUtilsTest.java @@ -24,6 +24,7 @@ package terrasaur.utils.math; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; + import java.util.List; import org.apache.commons.math3.geometry.euclidean.threed.Rotation; import org.apache.commons.math3.geometry.euclidean.threed.RotationConvention; @@ -31,179 +32,175 @@ import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; import org.apache.commons.math3.util.Pair; import org.junit.BeforeClass; import org.junit.Test; +import spice.basic.SpiceErrorException; import terrasaur.utils.Log4j2Configurator; import terrasaur.utils.VectorUtils; -import spice.basic.SpiceErrorException; public class RotationUtilsTest { - @BeforeClass - public static void setup() throws SpiceErrorException { - Log4j2Configurator.getInstance(); - } + @BeforeClass + public static void setup() throws SpiceErrorException { + Log4j2Configurator.getInstance(); + } - @Test - public void testIPrimaryJSecondary() { - Vector3D iInOldFrame = VectorUtils.randomVector(); - Vector3D jInOldFrame = VectorUtils.randomVector(); + @Test + public void testIPrimaryJSecondary() { + Vector3D iInOldFrame = VectorUtils.randomVector(); + Vector3D jInOldFrame = VectorUtils.randomVector(); - Rotation oldFrameToNewFrame = RotationUtils.IprimaryJsecondary(iInOldFrame, jInOldFrame); + Rotation oldFrameToNewFrame = RotationUtils.IprimaryJsecondary(iInOldFrame, jInOldFrame); - // this should be Vector3D.PLUS_I - Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_I)) < RotationUtils.THRESHOLD); + // this should be Vector3D.PLUS_I + Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_I)) < RotationUtils.THRESHOLD); - // this should be iInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); - assertTrue(Math.abs(Vector3D.angle(v, iInOldFrame)) < RotationUtils.THRESHOLD); + // this should be iInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); + assertTrue(Math.abs(Vector3D.angle(v, iInOldFrame)) < RotationUtils.THRESHOLD); - // Check that the zAxis is orthogonal to iInOldFrame and jInOldFrame - Vector3D zAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); - assertTrue(Math.abs(iInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(jInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); - } + // Check that the zAxis is orthogonal to iInOldFrame and jInOldFrame + Vector3D zAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); + assertTrue(Math.abs(iInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(jInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); + } - @Test - public void testIPrimaryKSecondary() { - Vector3D iInOldFrame = VectorUtils.randomVector(); - Vector3D kInOldFrame = VectorUtils.randomVector(); + @Test + public void testIPrimaryKSecondary() { + Vector3D iInOldFrame = VectorUtils.randomVector(); + Vector3D kInOldFrame = VectorUtils.randomVector(); - Rotation oldFrameToNewFrame = RotationUtils.IprimaryKsecondary(iInOldFrame, kInOldFrame); + Rotation oldFrameToNewFrame = RotationUtils.IprimaryKsecondary(iInOldFrame, kInOldFrame); - // this should be Vector3D.PLUS_I - Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_I)) < RotationUtils.THRESHOLD); + // this should be Vector3D.PLUS_I + Vector3D v = oldFrameToNewFrame.applyTo(iInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_I)) < RotationUtils.THRESHOLD); - // this should be iInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); - assertTrue(Math.abs(Vector3D.angle(v, iInOldFrame)) < RotationUtils.THRESHOLD); + // this should be iInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); + assertTrue(Math.abs(Vector3D.angle(v, iInOldFrame)) < RotationUtils.THRESHOLD); - // Check that the yAxis is orthogonal to iInOldFrame and kInOldFrame - Vector3D yAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); - assertTrue(Math.abs(iInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(kInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); - } + // Check that the yAxis is orthogonal to iInOldFrame and kInOldFrame + Vector3D yAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); + assertTrue(Math.abs(iInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(kInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); + } - @Test - public void testJPrimaryISecondary() { - Vector3D jInOldFrame = VectorUtils.randomVector(); - Vector3D iInOldFrame = VectorUtils.randomVector(); + @Test + public void testJPrimaryISecondary() { + Vector3D jInOldFrame = VectorUtils.randomVector(); + Vector3D iInOldFrame = VectorUtils.randomVector(); - Rotation oldFrameToNewFrame = RotationUtils.JprimaryIsecondary(jInOldFrame, iInOldFrame); + Rotation oldFrameToNewFrame = RotationUtils.JprimaryIsecondary(jInOldFrame, iInOldFrame); - // this should be Vector3D.PLUS_J - Vector3D v = oldFrameToNewFrame.applyTo(jInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_J)) < RotationUtils.THRESHOLD); + // this should be Vector3D.PLUS_J + Vector3D v = oldFrameToNewFrame.applyTo(jInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_J)) < RotationUtils.THRESHOLD); - // this should be jInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); - assertTrue(Math.abs(Vector3D.angle(v, jInOldFrame)) < RotationUtils.THRESHOLD); + // this should be jInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); + assertTrue(Math.abs(Vector3D.angle(v, jInOldFrame)) < RotationUtils.THRESHOLD); - // Check that the zAxis is orthogonal to kInOldFrame and iInOldFrame - Vector3D zAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); - assertTrue(Math.abs(jInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(iInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); - } + // Check that the zAxis is orthogonal to kInOldFrame and iInOldFrame + Vector3D zAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); + assertTrue(Math.abs(jInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(iInOldFrame.dotProduct(zAxis)) < RotationUtils.THRESHOLD); + } - @Test - public void testJPrimaryKSecondary() { - Vector3D jInOldFrame = VectorUtils.randomVector(); - Vector3D kInOldFrame = VectorUtils.randomVector(); + @Test + public void testJPrimaryKSecondary() { + Vector3D jInOldFrame = VectorUtils.randomVector(); + Vector3D kInOldFrame = VectorUtils.randomVector(); - Rotation oldFrameToNewFrame = RotationUtils.JprimaryKsecondary(jInOldFrame, kInOldFrame); + Rotation oldFrameToNewFrame = RotationUtils.JprimaryKsecondary(jInOldFrame, kInOldFrame); - // this should be Vector3D.PLUS_J - Vector3D v = oldFrameToNewFrame.applyTo(jInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_J)) < RotationUtils.THRESHOLD); + // this should be Vector3D.PLUS_J + Vector3D v = oldFrameToNewFrame.applyTo(jInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_J)) < RotationUtils.THRESHOLD); - // this should be jInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); - assertTrue(Math.abs(Vector3D.angle(v, jInOldFrame)) < RotationUtils.THRESHOLD); + // this should be jInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); + assertTrue(Math.abs(Vector3D.angle(v, jInOldFrame)) < RotationUtils.THRESHOLD); - // Check that the xAxis is orthogonal to iInOldFrame and kInOldFrame - Vector3D xAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); - assertTrue(Math.abs(jInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(kInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); - } + // Check that the xAxis is orthogonal to iInOldFrame and kInOldFrame + Vector3D xAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); + assertTrue(Math.abs(jInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(kInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); + } + @Test + public void testKPrimaryISecondary() { + Vector3D kInOldFrame = VectorUtils.randomVector(); + Vector3D iInOldFrame = VectorUtils.randomVector(); - @Test - public void testKPrimaryISecondary() { - Vector3D kInOldFrame = VectorUtils.randomVector(); - Vector3D iInOldFrame = VectorUtils.randomVector(); + Rotation oldFrameToNewFrame = RotationUtils.KprimaryIsecondary(kInOldFrame, iInOldFrame); - Rotation oldFrameToNewFrame = RotationUtils.KprimaryIsecondary(kInOldFrame, iInOldFrame); + // this should be Vector3D.PLUS_K + Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_K)) < RotationUtils.THRESHOLD); - // this should be Vector3D.PLUS_K - Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_K)) < RotationUtils.THRESHOLD); + // this should be kInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); + assertTrue(Math.abs(Vector3D.angle(v, kInOldFrame)) < RotationUtils.THRESHOLD); - // this should be kInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); - assertTrue(Math.abs(Vector3D.angle(v, kInOldFrame)) < RotationUtils.THRESHOLD); + // Check that the yAxis is orthogonal to kInOldFrame and iInOldFrame + Vector3D yAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); + assertTrue(Math.abs(kInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(iInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); + } - // Check that the yAxis is orthogonal to kInOldFrame and iInOldFrame - Vector3D yAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_J); - assertTrue(Math.abs(kInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(iInOldFrame.dotProduct(yAxis)) < RotationUtils.THRESHOLD); - } + @Test + public void testKPrimaryJSecondary() { + Vector3D kInOldFrame = VectorUtils.randomVector(); + Vector3D jInOldFrame = VectorUtils.randomVector(); + Rotation oldFrameToNewFrame = RotationUtils.KprimaryJsecondary(kInOldFrame, jInOldFrame); - @Test - public void testKPrimaryJSecondary() { - Vector3D kInOldFrame = VectorUtils.randomVector(); - Vector3D jInOldFrame = VectorUtils.randomVector(); + // this should be Vector3D.PLUS_K + Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame); + assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_K)) < RotationUtils.THRESHOLD); - Rotation oldFrameToNewFrame = RotationUtils.KprimaryJsecondary(kInOldFrame, jInOldFrame); + // this should be kInOldFrame + v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); + assertTrue(Math.abs(Vector3D.angle(v, kInOldFrame)) < RotationUtils.THRESHOLD); - // this should be Vector3D.PLUS_K - Vector3D v = oldFrameToNewFrame.applyTo(kInOldFrame); - assertTrue(Math.abs(Vector3D.angle(v, Vector3D.PLUS_K)) < RotationUtils.THRESHOLD); + // Check that the xAxis is orthogonal to kInOldFrame and iInOldFrame + Vector3D xAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); + assertTrue(Math.abs(kInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); + assertTrue(Math.abs(jInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); + } - // this should be kInOldFrame - v = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_K); - assertTrue(Math.abs(Vector3D.angle(v, kInOldFrame)) < RotationUtils.THRESHOLD); + @Test + public void testStringToRotation() { - // Check that the xAxis is orthogonal to kInOldFrame and iInOldFrame - Vector3D xAxis = oldFrameToNewFrame.applyInverseTo(Vector3D.PLUS_I); - assertTrue(Math.abs(kInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); - assertTrue(Math.abs(jInOldFrame.dotProduct(xAxis)) < RotationUtils.THRESHOLD); - } + RotationConvention rc = RotationConvention.FRAME_TRANSFORM; - @Test - public void testStringToRotation() { + Rotation r = RotationUtils.randomRotation(); + Vector3D axis = r.getAxis(rc); + double angle = r.getAngle(); - RotationConvention rc = RotationConvention.FRAME_TRANSFORM; + String s = RotationUtils.rotationToString(r); - Rotation r = RotationUtils.randomRotation(); - Vector3D axis = r.getAxis(rc); - double angle = r.getAngle(); + r = RotationUtils.stringToRotation(s); - String s = RotationUtils.rotationToString(r); + assertTrue(axis.dotProduct(r.getAxis(rc)) > 1 - RotationUtils.THRESHOLD); + assertEquals(angle, r.getAngle(), RotationUtils.THRESHOLD); + } - r = RotationUtils.stringToRotation(s); + @Test + public void testStringToTransform() { + RotationConvention rc = RotationConvention.FRAME_TRANSFORM; - assertTrue(axis.dotProduct(r.getAxis(rc)) > 1 - RotationUtils.THRESHOLD); - assertEquals(angle, r.getAngle(), RotationUtils.THRESHOLD); + Vector3D v = VectorUtils.randomVector(); + Rotation r = RotationUtils.randomRotation(); + Vector3D axis = r.getAxis(rc); + double angle = r.getAngle(); - } + List lines = RotationUtils.transformToString(v, r); - @Test - public void testStringToTransform() { - RotationConvention rc = RotationConvention.FRAME_TRANSFORM; - - Vector3D v = VectorUtils.randomVector(); - Rotation r = RotationUtils.randomRotation(); - Vector3D axis = r.getAxis(rc); - double angle = r.getAngle(); - - List lines = RotationUtils.transformToString(v, r); - - Pair p = RotationUtils.stringToTransform(lines); - - assertTrue(v.dotProduct(p.getFirst()) > 1 - RotationUtils.THRESHOLD); - assertTrue(axis.dotProduct(p.getSecond().getAxis(rc)) > 1 - RotationUtils.THRESHOLD); - assertEquals(angle, p.getSecond().getAngle(), RotationUtils.THRESHOLD); - } + Pair p = RotationUtils.stringToTransform(lines); + assertTrue(v.dotProduct(p.getFirst()) > 1 - RotationUtils.THRESHOLD); + assertTrue(axis.dotProduct(p.getSecond().getAxis(rc)) > 1 - RotationUtils.THRESHOLD); + assertEquals(angle, p.getSecond().getAngle(), RotationUtils.THRESHOLD); + } } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlotTest.java b/src/test/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlotTest.java index 04c5cfa..c76e09a 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlotTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/canvas/DiscreteDataPlotTest.java @@ -31,26 +31,29 @@ import terrasaur.utils.saaPlotLib.config.PlotConfig; public class DiscreteDataPlotTest { - @Test - public void testBlank() { - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).build(); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - BufferedImage image = canvas.getImage(); - // PlotCanvas.writeImage("blank.png", image); - } + @Test + public void testBlank() { + PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).build(); + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + BufferedImage image = canvas.getImage(); + // PlotCanvas.writeImage("blank.png", image); + } - @Test - public void testAnnotation() { - PlotConfig config = ImmutablePlotConfig.builder().width(800).height(600).leftMargin(160) - .bottomMargin(160).build(); + @Test + public void testAnnotation() { + PlotConfig config = ImmutablePlotConfig.builder() + .width(800) + .height(600) + .leftMargin(160) + .bottomMargin(160) + .build(); - DiscreteDataPlot canvas = new DiscreteDataPlot(config); - - AxisX xAxis = new AxisX(0, 1, "Line 1\nLine2\nLine3"); - AxisY yAxis = new AxisY(0, 1, "Line 1\nLine2\nLine3"); - canvas.setAxes(xAxis, yAxis); - canvas.drawAxes(); - // PlotCanvas.writeImage("annotation.png", canvas.getImage()); - } + DiscreteDataPlot canvas = new DiscreteDataPlot(config); + AxisX xAxis = new AxisX(0, 1, "Line 1\nLine2\nLine3"); + AxisY yAxis = new AxisY(0, 1, "Line 1\nLine2\nLine3"); + canvas.setAxes(xAxis, yAxis); + canvas.drawAxes(); + // PlotCanvas.writeImage("annotation.png", canvas.getImage()); + } } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRangeTest.java b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRangeTest.java index 442606d..7be5bbc 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRangeTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/AxisRangeTest.java @@ -29,63 +29,62 @@ import org.junit.Test; public class AxisRangeTest { - @Test - public void testLinear() { + @Test + public void testLinear() { - double begin = 0.01234567; - double end = 12.34567; + double begin = 0.01234567; + double end = 12.34567; - double tolerance = 1e-12; + double tolerance = 1e-12; - AxisRange range = AxisRange.getLinearAxis(begin, end, 1); - assertNotNull(range); - assertEquals(range.getBegin(), 0.01, tolerance); - assertEquals(range.getEnd(), 20, tolerance); + AxisRange range = AxisRange.getLinearAxis(begin, end, 1); + assertNotNull(range); + assertEquals(range.getBegin(), 0.01, tolerance); + assertEquals(range.getEnd(), 20, tolerance); - range = AxisRange.getLinearAxis(begin, end, 2); - assertNotNull(range); - assertEquals(range.getBegin(), 0.012, tolerance); - assertEquals(range.getEnd(), 13, tolerance); + range = AxisRange.getLinearAxis(begin, end, 2); + assertNotNull(range); + assertEquals(range.getBegin(), 0.012, tolerance); + assertEquals(range.getEnd(), 13, tolerance); - range = AxisRange.getLinearAxis(begin, end, 3); - assertNotNull(range); - assertEquals(range.getBegin(), 0.0123, tolerance); - assertEquals(range.getEnd(), 12.4, tolerance); + range = AxisRange.getLinearAxis(begin, end, 3); + assertNotNull(range); + assertEquals(range.getBegin(), 0.0123, tolerance); + assertEquals(range.getEnd(), 12.4, tolerance); - range = AxisRange.getLinearAxis(begin, end, 4); - assertNotNull(range); - assertEquals(range.getBegin(), 0.01234, tolerance); - assertEquals(range.getEnd(), 12.35, tolerance); + range = AxisRange.getLinearAxis(begin, end, 4); + assertNotNull(range); + assertEquals(range.getBegin(), 0.01234, tolerance); + assertEquals(range.getEnd(), 12.35, tolerance); - range = AxisRange.getLinearAxis(begin, end, 5); - assertNotNull(range); - assertEquals(range.getBegin(), 0.012345, tolerance); - assertEquals(range.getEnd(), 12.346, tolerance); + range = AxisRange.getLinearAxis(begin, end, 5); + assertNotNull(range); + assertEquals(range.getBegin(), 0.012345, tolerance); + assertEquals(range.getEnd(), 12.346, tolerance); - range = AxisRange.getLinearAxis(begin, end, 6); - assertNotNull(range); - assertEquals(range.getBegin(), 0.0123456, tolerance); - assertEquals(range.getEnd(), 12.3457, tolerance); - } + range = AxisRange.getLinearAxis(begin, end, 6); + assertNotNull(range); + assertEquals(range.getBegin(), 0.0123456, tolerance); + assertEquals(range.getEnd(), 12.3457, tolerance); + } - @Test - public void testLog() { - double tolerance = 1e-12; + @Test + public void testLog() { + double tolerance = 1e-12; - AxisRange range = AxisRange.getLogAxis(1.33, 4.55); - assertNotNull(range); - assertEquals(range.getBegin(), 1, tolerance); - assertEquals(range.getEnd(), 10, tolerance); + AxisRange range = AxisRange.getLogAxis(1.33, 4.55); + assertNotNull(range); + assertEquals(range.getBegin(), 1, tolerance); + assertEquals(range.getEnd(), 10, tolerance); - range = AxisRange.getLogAxis(1.33, 45.5); - assertNotNull(range); - assertEquals(range.getBegin(), 1, tolerance); - assertEquals(range.getEnd(), 100, tolerance); - - range = AxisRange.getLogAxis(.0133, 455); - assertNotNull(range); - assertEquals(range.getBegin(), 0.01, tolerance); - assertEquals(range.getEnd(), 1000, tolerance); - } + range = AxisRange.getLogAxis(1.33, 45.5); + assertNotNull(range); + assertEquals(range.getBegin(), 1, tolerance); + assertEquals(range.getEnd(), 100, tolerance); + range = AxisRange.getLogAxis(.0133, 455); + assertNotNull(range); + assertEquals(range.getBegin(), 0.01, tolerance); + assertEquals(range.getEnd(), 1000, tolerance); + } } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarksTest.java b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarksTest.java index 5a2d970..adb9641 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarksTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/TickMarksTest.java @@ -29,30 +29,28 @@ import org.junit.Test; public class TickMarksTest { - @Test - public void test1() { - double begin = 0.1; - double end = 1e5; + @Test + public void test1() { + double begin = 0.1; + double end = 1e5; - boolean isLog = true; + boolean isLog = true; - TickMarks tm = new TickMarks(new AxisRange(begin, end), isLog); + TickMarks tm = new TickMarks(new AxisRange(begin, end), isLog); - NavigableSet majorTicks = tm.getMajorTicks(); - NavigableSet minorTicks = tm.getMinorTicks(); + NavigableSet majorTicks = tm.getMajorTicks(); + NavigableSet minorTicks = tm.getMinorTicks(); - NavigableMap ticks = new TreeMap<>(); - for (Double majorTick : majorTicks) { - ticks.put(majorTick, String.format("Major tick: %f", majorTick)); + NavigableMap ticks = new TreeMap<>(); + for (Double majorTick : majorTicks) { + ticks.put(majorTick, String.format("Major tick: %f", majorTick)); + } + for (Double minorTick : minorTicks) { + if (!ticks.containsKey(minorTick)) ticks.put(minorTick, String.format("Minor tick:\t %f", minorTick)); + } + + for (String tick : ticks.values()) { + System.out.println(tick); + } } - for (Double minorTick : minorTicks) { - if (!ticks.containsKey(minorTick)) - ticks.put(minorTick, String.format("Minor tick:\t %f", minorTick)); - } - - for (String tick : ticks.values()) { - System.out.println(tick); - } - } - } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisTest.java b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisTest.java index 4721b32..06e3f51 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/canvas/axis/UTCAxisTest.java @@ -30,68 +30,62 @@ import picante.time.TimeConversion; public class UTCAxisTest { - @Test - public void test1() { - TimeConversion tc = TimeConversion.createUsingInternalConstants(); + @Test + public void test1() { + TimeConversion tc = TimeConversion.createUsingInternalConstants(); - double min = tc.utcStringToTDB("2035-11-05T16:02:17.782"); - double max = tc.utcStringToTDB("2036-01-24T12:30:17.780"); + double min = tc.utcStringToTDB("2035-11-05T16:02:17.782"); + double max = tc.utcStringToTDB("2036-01-24T12:30:17.780"); - UTCAxisX axis = new UTCAxisX(min, max, "UTC", 9); + UTCAxisX axis = new UTCAxisX(min, max, "UTC", 9); - NavigableSet majorTicks = axis.getMajorTicks(); - NavigableSet minorTicks = axis.getMinorTicks(); + NavigableSet majorTicks = axis.getMajorTicks(); + NavigableSet minorTicks = axis.getMinorTicks(); - System.out.println(majorTicks.size() + " major ticks"); + System.out.println(majorTicks.size() + " major ticks"); - NavigableMap ticks = new TreeMap<>(); - int count = 0; - for (Double majorTick : majorTicks) { - ticks.put(majorTick, String.format("%d %f", count++, majorTick)); - } - for (Double minorTick : minorTicks) { - if (!ticks.containsKey(minorTick)) - ticks.put(minorTick, String.format("\t%f", minorTick)); + NavigableMap ticks = new TreeMap<>(); + int count = 0; + for (Double majorTick : majorTicks) { + ticks.put(majorTick, String.format("%d %f", count++, majorTick)); + } + for (Double minorTick : minorTicks) { + if (!ticks.containsKey(minorTick)) ticks.put(minorTick, String.format("\t%f", minorTick)); + } + + for (Double tick : ticks.keySet()) { + System.out.printf("%-20s %s\n", ticks.get(tick), tc.tdbToUTC(tick).toString()); + } + + NavigableMap tickLabels = axis.getTickLabels(); + for (Double tick : tickLabels.keySet()) System.out.printf("%f %s\n", tick, tickLabels.get(tick)); } - for (Double tick : ticks.keySet()) { - System.out.printf("%-20s %s\n", ticks.get(tick), tc.tdbToUTC(tick).toString()); + @Test + public void test2() { + TimeConversion tc = TimeConversion.createUsingInternalConstants(); + + double min = 0; + double max = 50 * 365.25 * 86400.; + + UTCAxisX axis = new UTCAxisX(min, max, "UTC", 5); + + NavigableSet majorTicks = axis.getMajorTicks(); + NavigableSet minorTicks = axis.getMinorTicks(); + + System.out.println(majorTicks.size() + " major ticks"); + + NavigableMap ticks = new TreeMap<>(); + int count = 0; + for (Double majorTick : majorTicks) { + ticks.put(majorTick, String.format("%d %f", count++, majorTick)); + } + for (Double minorTick : minorTicks) { + if (!ticks.containsKey(minorTick)) ticks.put(minorTick, String.format("\t%f", minorTick)); + } + + for (Double tick : ticks.keySet()) { + System.out.printf("%-20s %s\n", ticks.get(tick), tc.tdbToUTC(tick).toString()); + } } - - NavigableMap tickLabels = axis.getTickLabels(); - for (Double tick : tickLabels.keySet()) - System.out.printf("%f %s\n", tick, tickLabels.get(tick)); - - } - - @Test - public void test2() { - TimeConversion tc = TimeConversion.createUsingInternalConstants(); - - double min = 0; - double max = 50 * 365.25 * 86400.; - - UTCAxisX axis = new UTCAxisX(min, max, "UTC", 5); - - NavigableSet majorTicks = axis.getMajorTicks(); - NavigableSet minorTicks = axis.getMinorTicks(); - - System.out.println(majorTicks.size() + " major ticks"); - - NavigableMap ticks = new TreeMap<>(); - int count = 0; - for (Double majorTick : majorTicks) { - ticks.put(majorTick, String.format("%d %f", count++, majorTick)); - } - for (Double minorTick : minorTicks) { - if (!ticks.containsKey(minorTick)) - ticks.put(minorTick, String.format("\t%f", minorTick)); - } - - for (Double tick : ticks.keySet()) { - System.out.printf("%-20s %s\n", ticks.get(tick), tc.tdbToUTC(tick).toString()); - } - - } - } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRampTest.java b/src/test/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRampTest.java index f29979b..5370028 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRampTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/colorMaps/DivergentColorRampTest.java @@ -28,28 +28,28 @@ import org.junit.Test; public class DivergentColorRampTest { - @Test - public void test01() { + @Test + public void test01() { - // like matplotlib's "coolwarm" - Color minColor = new Color(59, 76, 192); - Color maxColor = new Color(180, 4, 38); + // like matplotlib's "coolwarm" + Color minColor = new Color(59, 76, 192); + Color maxColor = new Color(180, 4, 38); - // like matplotlib's "RdBu" - // minColor = new Color(100,0,31); - // maxColor = new Color(5,48,97); + // like matplotlib's "RdBu" + // minColor = new Color(100,0,31); + // maxColor = new Color(5,48,97); - // like matplotlib's "PuOr" - minColor = new Color(124, 58, 20); - maxColor = new Color(44, 9, 74); + // like matplotlib's "PuOr" + minColor = new Color(124, 58, 20); + maxColor = new Color(44, 9, 74); - DivergentColorRamp dcr = new DivergentColorRamp(); - // maxColor = dcr.estimateEndColor(minColor); + DivergentColorRamp dcr = new DivergentColorRamp(); + // maxColor = dcr.estimateEndColor(minColor); - List colors = dcr.generateColorMap(minColor, maxColor, 33); + List colors = dcr.generateColorMap(minColor, maxColor, 33); - DivergentColorRamp.test(colors); - // PlotCanvas.showJFrame(dcr.deltaEPlot(colors)); + DivergentColorRamp.test(colors); + // PlotCanvas.showJFrame(dcr.deltaEPlot(colors)); - } + } } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/data/PointListTest.java b/src/test/java/terrasaur/utils/saaPlotLib/data/PointListTest.java index 6da69e9..ca0cb9c 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/data/PointListTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/data/PointListTest.java @@ -30,7 +30,7 @@ public class PointListTest { @Ignore @Test - public void test01(){ + public void test01() { Random r = new Random(); PointList pl = new PointList(); for (int i = 0; i < 100; i++) { @@ -46,5 +46,4 @@ public class PointListTest { System.out.println(subSet.getY(0.5)); System.out.println(pl.getY(0.5)); } - } diff --git a/src/test/java/terrasaur/utils/saaPlotLib/util/PlotUtilsTest.java b/src/test/java/terrasaur/utils/saaPlotLib/util/PlotUtilsTest.java index 5ebe6c0..d6810ca 100644 --- a/src/test/java/terrasaur/utils/saaPlotLib/util/PlotUtilsTest.java +++ b/src/test/java/terrasaur/utils/saaPlotLib/util/PlotUtilsTest.java @@ -78,5 +78,4 @@ public class PlotUtilsTest { assertTrue(Math.abs(PlotUtils.getRoundFloor(number, 6) + 434.789) < 0.0005); assertTrue(Math.abs(PlotUtils.getRoundFloor(number, 7) + 434.7882) < 0.00005); } - }