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}