001/* 002 * GenModelNote -- Partial implementation of a note that is displayed at a fixed size in model space. 003 * 004 * Copyright (C) 2014-2015, Joseph A. Huwaldt 005 * All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public License 018 * along with this program; if not, write to the Free Software 019 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 020 * Or visit: http://www.gnu.org/licenses/lgpl.html 021 */ 022package geomss.geom; 023 024import jahuwaldt.js.param.DCMatrix; 025import jahuwaldt.js.param.Parameter; 026import java.awt.Canvas; 027import java.awt.Font; 028import java.awt.FontMetrics; 029import static java.util.Objects.nonNull; 030import static java.util.Objects.requireNonNull; 031import javax.measure.quantity.Dimensionless; 032import javax.measure.quantity.Length; 033import javolution.context.StackContext; 034import javolution.text.Text; 035import javolution.text.TextBuilder; 036import org.jscience.mathematics.vector.Float64Vector; 037 038/** 039 * Partial implementation of a textual note located at a point in nD space, and 040 * represented at a fixed size and orientation in model space. 041 * 042 * <p> Modified by: Joseph A. Huwaldt </p> 043 * 044 * @author Joseph A. Huwaldt, Date: February 10, 2014 045 * @version November 24, 2015 046 */ 047@SuppressWarnings({"serial", "CloneableImplementsClone"}) 048public abstract class GenModelNote extends AbstractNote<GenModelNote> implements Transformable<GenModelNote> { 049 050 /** 051 * Return an immutable version of this note. 052 * 053 * @return An immutable version of this note. 054 */ 055 public abstract ModelNote immutable(); 056 057 /** 058 * Return the vector indicating the horizontal axis direction for the text. 059 * 060 * @return The vector indicating the horizontal axis direction for the text. 061 */ 062 public abstract GeomVector<Dimensionless> getXHat(); 063 064 /** 065 * Return the vector indicating the vertical axis direction (or ascent direction) for 066 * the text. 067 * 068 * @return The vector indicating the vertical axis direction (or ascent direction) for 069 * the text. 070 */ 071 public abstract GeomVector<Dimensionless> getYHat(); 072 073 /** 074 * Return the (at least 3D) vector indicating the normal axis direction for the text. 075 * When looking down the normal axis, you are looking at the text face-on. 076 * 077 * @return The (at least 3D) vector indicating the normal axis direction for the text. 078 */ 079 public Vector<Dimensionless> getNormal() { 080 if (getPhyDimension() < 3) 081 return Vector.valueOf(0, 0, 1); 082 return getXHat().cross(getYHat()).toUnitVector(); 083 } 084 085 /** 086 * Return the height of the text box in model units. 087 * 088 * @return The height of the text box in model units. 089 */ 090 public abstract Parameter<Length> getHeight(); 091 092 /** 093 * Return the width of the text box in model units. 094 * 095 * @return The width of the text box in model units. 096 */ 097 public Parameter<Length> getWidth() { 098 Font font = getFont(); 099 font.deriveFont(font.getSize() * 4f); // Scale up the font a bit to make it sharper in the rendering. 100 FontMetrics fm = new Canvas().getFontMetrics(font); 101 int height = fm.getMaxAscent() + fm.getMaxDescent(); 102 int width = fm.stringWidth(getNote()); 103 104 // Need to make width/height powers of 2 because of Java3d texture 105 // size restrictions 106 int pow = 1; 107 for (int i = 1; i < 32; ++i) { 108 pow *= 2; 109 if (width <= pow) 110 break; 111 } 112 width = Math.max(width, pow); 113 pow = 1; 114 for (int i = 1; i < 32; ++i) { 115 pow *= 2; 116 if (height <= pow) 117 break; 118 } 119 height = Math.max(height, pow); 120 121 Parameter<Length> heightParam = getHeight(); 122 double modelHeight = heightParam.getValue(); 123 double scale = modelHeight / height; 124 return Parameter.valueOf(width * scale, heightParam.getUnit()); 125 } 126 127 /** 128 * Return the descent of the text below the baseline (text location) in model units. 129 * This is the offset, in the yhat direction, of the "lower-left" corner of the 130 * bounding box from the text string baseline (which passes through the location 131 * point). 132 */ 133 private Parameter<Length> getDescent() { 134 Font font = getFont(); 135 font.deriveFont(font.getSize() * 4f); // Scale up the font a bit to make it sharper in the rendering. 136 FontMetrics fm = new Canvas().getFontMetrics(font); 137 int ascent = fm.getMaxAscent(); 138 int descent = fm.getMaxDescent(); 139 int height = ascent + descent; 140 141 // Need to make height a power of 2 because of Java3D texture 142 // size restrictions 143 int pow = 1; 144 for (int i = 1; i < 32; ++i) { 145 pow *= 2; 146 if (height <= pow) 147 break; 148 } 149 height = Math.max(height, pow); 150 151 Parameter<Length> heightParam = getHeight(); 152 double modelHeight = heightParam.getValue(); 153 double scale = modelHeight / height; 154 return Parameter.valueOf(descent * scale, heightParam.getUnit()); 155 } 156 157 /** 158 * Return a new note object identical to this one, but with the specified height in 159 * model space. 160 * 161 * @param height The new height for the note in model space coordinates. May not be 162 * null. 163 * @return A new note object identical to this one, but with the specified height in 164 * model space. 165 */ 166 public abstract GenModelNote changeHeight(Parameter<Length> height); 167 168 /** 169 * Returns the number of child-elements that make up this geometry element. This 170 * implementation always returns 1 as a GenModelNote is located at a single point in 171 * model space. 172 */ 173 @Override 174 public int size() { 175 return 1; 176 } 177 178 /** 179 * Return the coordinate point representing the minimum bounding box corner (e.g.: min 180 * X, min Y, min Z). 181 * 182 * @return The minimum bounding box coordinate for this geometry element. 183 */ 184 @Override 185 public Point getBoundsMin() { 186 // Compute opposite corners of the text box. 187 188 StackContext.enter(); 189 try { 190 // Get the parameters of this note. 191 Parameter<Length> height = getHeight(); 192 Parameter<Length> width = getWidth(); 193 Parameter<Length> descent = getDescent(); 194 GeomVector<Dimensionless> xhat = getXHat(); 195 GeomVector<Dimensionless> yhat = getYHat(); 196 197 // Get the "lower-left" corner of the text box. 198 Point p0 = getLocation().minus(Point.valueOf(yhat.times(descent))); 199 200 // Compute the "upper-right" corner of the text box. 201 Point p1 = p0.plus(Point.valueOf(xhat.times(width).plus((GeomVector)yhat.times(height)))); 202 203 Point bounds = p0.min(p1); 204 return StackContext.outerCopy(bounds); 205 206 } finally { 207 StackContext.exit(); 208 } 209 } 210 211 /** 212 * Return the coordinate point representing the maximum bounding box corner (e.g.: max 213 * X, max Y, max Z). 214 * 215 * @return The maximum bounding box coordinate for this geometry element. 216 */ 217 @Override 218 public Point getBoundsMax() { 219 // Compute opposite corners of the text box. 220 221 StackContext.enter(); 222 try { 223 // Get the parameters of this note. 224 Parameter<Length> height = getHeight(); 225 Parameter<Length> width = getWidth(); 226 Parameter<Length> descent = getDescent(); 227 GeomVector<Dimensionless> xhat = getXHat(); 228 GeomVector<Dimensionless> yhat = getYHat(); 229 230 // Get the "lower-left" corner of the text box. 231 Point p0 = getLocation().minus(Point.valueOf(yhat.times(descent))); 232 233 // Compute the "upper-right" corner of the text box. 234 Point p1 = p0.plus(Point.valueOf(xhat.times(width).plus((GeomVector)yhat.times(height)))); 235 236 Point bounds = p0.max(p1); 237 return StackContext.outerCopy(bounds); 238 239 } finally { 240 StackContext.exit(); 241 } 242 } 243 244 /** 245 * Returns the most extreme point, either minimum or maximum, in the specified 246 * coordinate direction on this geometry element. 247 * 248 * @param dim An index indicating the dimension to find the min/max point for 249 * (0=X,1=Y, 2=Z, etc). 250 * @param max Set to <code>true</code> to return the maximum value, <code>false</code> 251 * to return the minimum. 252 * @param tol Fractional tolerance to refine the min/max point position to if 253 * necessary. 254 * @return The point found on this element that is the min or max in the specified 255 * coordinate direction. 256 * @see #getBoundsMin 257 * @see #getBoundsMax 258 */ 259 @Override 260 public Point getLimitPoint(int dim, boolean max, double tol) { 261 // Compute all 4 corners of the text box. 262 263 StackContext.enter(); 264 try { 265 // Get the parameters of this note. 266 Parameter<Length> height = getHeight(); 267 Parameter<Length> width = getWidth(); 268 Parameter<Length> descent = getDescent(); 269 GeomVector<Dimensionless> xhat = getXHat(); 270 GeomVector<Dimensionless> yhat = getYHat(); 271 272 // Get the "lower-left" corner of the text box. 273 Point p0 = getLocation().minus(Point.valueOf(yhat.times(descent))); 274 275 // Compute the "lower-right" corner of the text box. 276 Vector projWidth = xhat.times(width); 277 Point p1 = p0.plus(Point.valueOf(projWidth)); 278 279 // Compute the "upper-right" corner of the text box. 280 Vector projHeight = yhat.times(height); 281 Point p2 = p0.plus(Point.valueOf(projWidth.plus(projHeight))); 282 283 // Compute the "upper-left" corner of the text box. 284 Point p3 = p0.plus(Point.valueOf(projHeight)); 285 286 // Compare the corner points to find the limiting point. 287 Point limPnt = p0; 288 Parameter<Length> lim = limPnt.get(dim); 289 if ((max && p1.get(dim).isGreaterThan(lim)) || (!max && p1.get(dim).isLessThan(lim))) { 290 limPnt = p1; 291 lim = limPnt.get(dim); 292 } 293 if ((max && p2.get(dim).isGreaterThan(lim)) || (!max && p2.get(dim).isLessThan(lim))) { 294 limPnt = p2; 295 lim = limPnt.get(dim); 296 } 297 if ((max && p3.get(dim).isGreaterThan(lim)) || (!max && p3.get(dim).isLessThan(lim))) { 298 limPnt = p3; 299 } 300 301 return StackContext.outerCopy(limPnt); 302 303 } finally { 304 StackContext.exit(); 305 } 306 } 307 308 /** 309 * Return a direction cosine matrix containing the orientation of the note string. 310 * 311 * @return A direction cosine matrix containing the orientation of the note string. 312 */ 313 public DCMatrix getOrientation() { 314 Float64Vector col0 = getXHat().toFloat64Vector(); 315 Float64Vector col1 = getYHat().toFloat64Vector(); 316 Float64Vector col2 = getNormal().toFloat64Vector(); 317 DCMatrix M = DCMatrix.valueOf(col0, col1, col2).transpose(); 318 return M; 319 } 320 321 /** 322 * Returns transformed version of this element. The returned object implements 323 * {@link GeomTransform} and contains this element as a child. 324 * 325 * @param transform The transformation to apply to this geometry. May not be null. 326 * @return A new triangle that is identical to this one with the specified 327 * transformation applied. 328 * @throws DimensionException if this point is not 3D. 329 */ 330 @Override 331 public ModelNoteTrans getTransformed(GTransform transform) { 332 return ModelNoteTrans.newInstance(this, requireNonNull(transform)); 333 } 334 335 /** 336 * Returns the text representation of this geometry element that consists of the text 337 * string, the orienting plane and location, and the text box height. For example: 338 * <pre> 339 * {aNote = {"A text string.",{1,0,0},{0,1,0},{10 ft, -3 ft, 4.56 ft},0.2 ft}} 340 * </pre> 341 * If there is no name, then the output looks like this: 342 * <pre> 343 * {"A text string.",{0,0,1},{0,1,0},{10 ft, -3 ft, 4.56 ft},0.2 ft} 344 * </pre> 345 * 346 * @return the text representation of this geometry element. 347 */ 348 @Override 349 public Text toText() { 350 TextBuilder tmp = TextBuilder.newInstance(); 351 tmp.append('{'); 352 String nameStr = getName(); 353 boolean hasName = nonNull(nameStr); 354 if (hasName) { 355 tmp.append(nameStr); 356 tmp.append(" = {"); 357 } 358 tmp.append("\""); 359 tmp.append(getNote()); 360 tmp.append("\","); 361 tmp.append(getXHat().toFloat64Vector().toText()); 362 tmp.append(","); 363 tmp.append(getYHat().toFloat64Vector().toText()); 364 tmp.append(","); 365 tmp.append(getLocation().toText()); 366 tmp.append(","); 367 tmp.append(getHeight().toText()); 368 if (hasName) 369 tmp.append('}'); 370 tmp.append('}'); 371 Text txt = tmp.toText(); 372 TextBuilder.recycle(tmp); 373 return txt; 374 } 375 376}