001/*
002 *   ModelNote  -- Holds a textual note String located at a point in model space with a specified
003 *                  size and orientation in model space.
004 *
005 *   Copyright (C) 2014-2025, Joseph A. Huwaldt
006 *   All rights reserved.
007 *   
008 *   This library is free software; you can redistribute it and/or
009 *   modify it under the terms of the GNU Lesser General Public
010 *   License as published by the Free Software Foundation; either
011 *   version 2.1 of the License, or (at your option) any later version.
012 *   
013 *   This library is distributed in the hope that it will be useful,
014 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
015 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
016 *   Lesser General Public License for more details.
017 *
018 *   You should have received a copy of the GNU Lesser General Public License
019 *   along with this program; if not, write to the Free Software
020 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
021 *   Or visit:  http://www.gnu.org/licenses/lgpl.html
022 */
023package geomss.geom;
024
025import jahuwaldt.js.param.DCMatrix;
026import jahuwaldt.js.param.Parameter;
027import jahuwaldt.js.param.Rotation;
028import java.awt.Font;
029import java.text.MessageFormat;
030import java.util.Objects;
031import static java.util.Objects.requireNonNull;
032import javax.measure.converter.ConversionException;
033import javax.measure.quantity.Dimensionless;
034import javax.measure.quantity.Length;
035import javax.measure.unit.Unit;
036import javolution.context.ObjectFactory;
037import javolution.lang.ValueType;
038import javolution.xml.XMLFormat;
039import javolution.xml.stream.XMLStreamException;
040import org.jscience.mathematics.vector.Float64Vector;
041
042/**
043 * Represents a textual note located at a point in model space with a specified size and
044 * orientation in model space.
045 *
046 * <p>
047 * Modified by: Joseph A. Huwaldt </p>
048 *
049 * @author Joseph A. Huwaldt, Date: February 10, 2014
050 * @version February 17, 2015
051 */
052@SuppressWarnings({"serial", "CloneableImplementsClone"})
053public final class ModelNote extends GenModelNote implements ValueType {
054
055    /**
056     * The text string displayed in the note.
057     */
058    private String text;
059
060    /**
061     * Holds the direction that the text flows in space.
062     */
063    private Vector<Dimensionless> xhat;
064
065    /**
066     * Holds the direction, normal to xhat, that represents the vertical direction of the
067     * text (the text ascent direction).
068     */
069    private Vector<Dimensionless> yhat;
070
071    /**
072     * Holds the location of the note in model space.
073     */
074    private Point location;
075
076    /**
077     * The font used to display the note.
078     */
079    private Font font;
080
081    /**
082     * The approximate height of the text in model units.
083     */
084    private Parameter<Length> height;
085
086    /**
087     * Construct and return a new instance of a ModelNote that uses the specified text
088     * string, the specified display font, is located at the location and orientation in
089     * space specified by the input vectors, and has the specified height in model units.
090     *
091     * @param text     The text to be displayed in this geometry object. May not be null.
092     * @param xhat     The vector indicating the direction that left-to-right text flows
093     *                 in space. May not be null.
094     * @param yhat     The vector, orthogonal to xhat, indicating the vertical direction
095     *                 (or ascent) of the text. May not be null.
096     * @param location The location of this geometry object in model space. May not be null.
097     * @param font     The font used to display this note. May not be null.
098     * @param height   The height of the text box in model units. May not be null.
099     * @return A new ModelNote using the specified inputs.
100     */
101    public static ModelNote valueOf(CharSequence text,
102            GeomVector<Dimensionless> xhat, GeomVector<Dimensionless> yhat, GeomPoint location,
103            Font font, Parameter<Length> height) {
104        requireNonNull(text);
105        requireNonNull(font);
106
107        //  Make sure the physical dimension and units are compatible.
108        Unit<Length> unit = height.getUnit();
109        int dim = GeomUtil.maxPhyDimension(requireNonNull(xhat), requireNonNull(yhat), requireNonNull(location));
110        if (dim < 2)
111            throw new IllegalArgumentException(
112                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "ModelNote", dim));
113        xhat = xhat.toDimension(dim);
114        yhat = yhat.toDimension(dim);
115        location = location.toDimension(dim).to(unit);
116
117        //  Make certain that yhat is orthogonal to xhat.
118        GeomVector n = xhat.cross(yhat);
119        yhat = n.cross(xhat).toUnitVector();
120
121        ModelNote note = FACTORY.object();
122        note.text = text.toString();
123        note.xhat = xhat.immutable();
124        note.yhat = yhat.immutable();
125        note.location = location.immutable();
126        note.font = font;
127        note.height = height;
128
129        return note;
130    }
131
132    /**
133     * Construct and return a new instance of a ModelNote that uses the specified text
134     * string, the default display font at the specified size, and is located at the
135     * location and orientation in space specified by the input plane.
136     *
137     * @param text     The text to be displayed in this geometry object. May not be null.
138     * @param xhat     The vector indicating the direction that left-to-right text flows
139     *                 in space. May not be null.
140     * @param yhat     The vector, orthogonal to xhat, indicating the vertical direction
141     *                 (or ascent) of the text. May not be null.
142     * @param location The location of this geometry object in model space. May not be null.
143     * @param height   The height of the text box in model units. May not be null.
144     * @return A new ModelNote using the specified inputs.
145     */
146    public static ModelNote valueOf(CharSequence text,
147            GeomVector<Dimensionless> xhat, GeomVector<Dimensionless> yhat, GeomPoint location,
148            Parameter<Length> height) {
149        return ModelNote.valueOf(text, xhat, yhat, location, DEFAULT_FONT, height);
150    }
151
152    /**
153     * Construct and return a new instance of a ModelNote that uses the specified text
154     * string, the default display font at the specified size, and is located parallel to
155     * the the XY plane with the xhat in the X-axis direction, yhat in the Y-axis
156     * direction.
157     *
158     * @param text     The text to be displayed in this geometry object. May not be null.
159     * @param location The location of this geometry object in model space. May not be null.
160     * @param height   The height of the text box in model units. May not be null.
161     * @return A new ModelNote using the specified inputs.
162     */
163    public static ModelNote valueOf(CharSequence text, GeomPoint location, Parameter<Length> height) {
164        int dim = location.getPhyDimension();
165        if (dim < 2)
166            throw new IllegalArgumentException(
167                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "ModelNote", dim));
168
169        Vector<Dimensionless> xhat = Vector.valueOf(1, 0).toDimension(dim);
170        Vector<Dimensionless> yhat = Vector.valueOf(0, 1).toDimension(dim);
171        return ModelNote.valueOf(text, xhat, yhat, location, DEFAULT_FONT, height);
172    }
173
174    /**
175     * Construct and return a new instance of a ModelNote that uses the specified text
176     * string, the default display font at the specified size, and is located at the
177     * specified location with the text plane oriented using the specified orientation
178     * rotation.
179     *
180     * @param text        The text to be displayed in this geometry object. May not be
181     *                    null.
182     * @param orientation The orientation of the text relative to X,Y &amp; Z 3D space axes.
183     *                    May not be null.
184     * @param location    The location of this geometry object in model space. The
185     *                    physical dimension must be &le; 3. If it is less than 3, it will
186     *                    be increased to 3. If &gt; 3, an exception is thrown. May not be
187     *                    null.
188     * @param height      The height of the text box in model units. May not be null.
189     * @throws IllegalArgumentException if the physical dimension of the input location
190     * point is greater than 3.
191     * @return A new ModelNote using the specified inputs.
192     */
193    public static ModelNote valueOf(CharSequence text, Rotation orientation, GeomPoint location, Parameter<Length> height) {
194        int dim = location.getPhyDimension();
195        if (dim < 3)
196            location = location.toDimension(3);
197        else if (dim > 3)
198            throw new IllegalArgumentException(
199                    MessageFormat.format(RESOURCES.getString("rotationDimensionLTE3"), dim));
200
201        //  Convert the arbitrary rotation to a direction-cosine matrix.
202        DCMatrix dcm = orientation.toDCM();
203
204        //  Convert the columns of the DCM matrix to xhat & yhat
205        Float64Vector col0 = dcm.getColumn(Point.X);
206        Float64Vector col1 = dcm.getColumn(Point.Y);
207        Vector<Dimensionless> xhat = Vector.valueOf(col0, Dimensionless.UNIT);
208        Vector<Dimensionless> yhat = Vector.valueOf(col1, Dimensionless.UNIT);
209
210        return ModelNote.valueOf(text, xhat, yhat, location, height);
211    }
212
213    /**
214     * Returns a new ModelNote instance that is identical to the specified ModelNote.
215     *
216     * @param note the ModelNote to be copied into a new ModelNote.  May not be null.
217     * @return A new ModelNote identical to the input note (a copy or clone).
218     */
219    public static ModelNote valueOf(ModelNote note) {
220        return copyOf(requireNonNull(note));
221    }
222
223    /**
224     * Return the text string associated with this note object.
225     *
226     * @return The text string associated with this note object.
227     */
228    @Override
229    public String getNote() {
230        return text;
231    }
232
233    /**
234     * Return the vector indicating the horizontal axis direction for the text.
235     *
236     * @return The vector indicating the horizontal axis direction for the text.
237     */
238    @Override
239    public Vector<Dimensionless> getXHat() {
240        return xhat;
241    }
242
243    /**
244     * Return the vector indicating the vertical axis direction (or ascent direction) for
245     * the text.
246     *
247     * @return The vector indicating the vertical axis direction (or ascent direction) for
248     *         the text.
249     */
250    @Override
251    public Vector<Dimensionless> getYHat() {
252        return yhat;
253    }
254
255    /**
256     * Return the location of this note in space.
257     *
258     * @return The location of this note in space.
259     */
260    @Override
261    public Point getLocation() {
262        return location;
263    }
264
265    /**
266     * Return the height of the text box in model units.
267     *
268     * @return The height of the text box in model units.
269     */
270    @Override
271    public Parameter<Length> getHeight() {
272        return height;
273    }
274
275    /**
276     * Return the font used to display this note.
277     *
278     * @return The font used to display this note.
279     */
280    @Override
281    public Font getFont() {
282        return font;
283    }
284
285    /**
286     * Return an immutable version of this note.
287     *
288     * @return An immutable version of this note.
289     */
290    @Override
291    public ModelNote immutable() {
292        return this;
293    }
294
295    /**
296     * Return a new note object identical to this one, but with the specified font.
297     *
298     * @param font The font for the new copy of this note.  May not be null.
299     * @return A new note object identical to this one, but with the specified font.
300     */
301    @Override
302    public ModelNote changeFont(Font font) {
303        ModelNote note = ModelNote.valueOf(text, xhat, yhat, location, requireNonNull(font), height);
304        copyState(note);
305        return note;
306    }
307
308    /**
309     * Return a new note object identical to this one, but with the specified location in
310     * model space. The returned note will have a different physical dimension from this
311     * one if the input location has a different physical dimension from the original
312     * location.
313     *
314     * @param location The location for the new copy of this note. May note be null.
315     * @return A new note object identical to this one, but with the specified location in
316     *         model space.
317     */
318    @Override
319    public ModelNote changeLocation(GeomPoint location) {
320        ModelNote note = ModelNote.valueOf(text, xhat, yhat, requireNonNull(location), font, height);
321        copyState(note);
322        return note;
323    }
324
325    /**
326     * Return a new note object identical to this one, but with the specified height in
327     * model space.
328     *
329     * @param height The height of the new copy of this note. May note be null.
330     * @return A new note object identical to this one, but with the specified height in
331     *         model space.
332     */
333    @Override
334    public ModelNote changeHeight(Parameter<Length> height) {
335        ModelNote note = ModelNote.valueOf(text, xhat, yhat, location, font, height.to(getUnit()));
336        copyState(note);
337        return note;
338    }
339
340    /**
341     * Returns the number of physical dimensions of the geometry element. This
342     * implementation will return the physical dimensions of the plane indicating the
343     * location and orientation of the note in space.
344     *
345     * @return The number of physical dimensions of the geometry element.
346     */
347    @Override
348    public int getPhyDimension() {
349        return location.getPhyDimension();
350    }
351
352    /**
353     * Return <code>true</code> if this ModelNote contains valid and finite numerical
354     * components. A value of <code>false</code> will be returned if any of the location
355     * coordinate values are NaN or Inf.
356     *
357     * @return true if this ModelNote contains valid and finite numerical components.
358     */
359    @Override
360    public boolean isValid() {
361        return xhat.isValid() && yhat.isValid() && location.isValid();
362    }
363
364    /**
365     * Returns a copy of this ModelNote instance
366     * {@link javolution.context.AllocatorContext allocated} by the calling thread
367     * (possibly on the stack).
368     *
369     * @return an identical and independent copy of this note.
370     */
371    @Override
372    public ModelNote copy() {
373        return copyOf(this);
374    }
375
376    /**
377     * Return a copy of this object with any transformations or subranges removed
378     * (applied).
379     *
380     * @return A copy of this object with any transformations or subranges removed.
381     */
382    @Override
383    public ModelNote copyToReal() {
384        return copy();
385    }
386
387    /**
388     * Returns the unit in which the note height and location are stored.
389     *
390     * @return The unit in which the note height and location are stored.
391     */
392    @Override
393    public final Unit<Length> getUnit() {
394        return height.getUnit();
395    }
396
397    /**
398     * Returns the equivalent to this note but with the location stated in the specified
399     * unit.
400     *
401     * @param unit the length unit of the note to be returned. May not be null.
402     * @return an equivalent of this note but with location stated in the specified unit.
403     * @throws ConversionException if the the input unit is not a length unit.
404     */
405    @Override
406    public ModelNote to(Unit<Length> unit) throws ConversionException {
407        if (unit.equals(getUnit()))
408            return this;
409
410        ModelNote note = FACTORY.object();
411        note.text = text;
412        note.xhat = xhat;
413        note.yhat = yhat;
414        note.location = location.to(unit);
415        note.height = height.to(unit);
416        copyState(note);
417
418        return note;
419    }
420
421    /**
422     * Return the equivalent of this note converted to the specified number of physical
423     * dimensions. If the number of dimensions is greater than this element, then zeros
424     * are added to the additional dimensions. If the number of dimensions is less than
425     * this element, then the extra dimensions are simply dropped (truncated). If the new
426     * dimensions are the same as the dimension of this element, then this element is
427     * simply returned.
428     *
429     * @param newDim The dimension of the note to return.
430     * @return The equivalent to this note converted to the new dimensions.
431     */
432    @Override
433    public ModelNote toDimension(int newDim) {
434        int thisDim = this.getPhyDimension();
435        if (newDim == thisDim)
436            return this;
437        if (newDim < 2)
438            throw new IllegalArgumentException(
439                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "ModelNote", newDim));
440
441        //  Convert the underlying geometry.
442        Vector<Dimensionless> newXHat = xhat.toDimension(newDim);
443        Vector<Dimensionless> newYHat = yhat.toDimension(newDim);
444        Point newLoc = location.toDimension(newDim);
445
446        //  Create and return a new note with the new geometry.
447        ModelNote note = ModelNote.valueOf(text, newXHat, newYHat, newLoc, font, height);
448        copyState(note);
449
450        return note;
451    }
452
453    /**
454     * Compares this ModelNote against the specified object for strict equality (same
455     * values and same units).
456     *
457     * @param obj the object to compare with.
458     * @return <code>true</code> if this note is identical to that note;
459     *         <code>false</code> otherwise.
460     */
461    @Override
462    public boolean equals(Object obj) {
463        if (this == obj)
464            return true;
465        if ((obj == null) || (obj.getClass() != this.getClass()))
466            return false;
467
468        ModelNote that = (ModelNote)obj;
469        return this.text.equals(that.text)
470                && this.height.equals(that.height)
471                && this.xhat.equals(that.xhat)
472                && this.yhat.equals(that.yhat)
473                && this.location.equals(that.location)
474                && super.equals(obj);
475    }
476
477    /**
478     * Returns the hash code for this ModelNote object.
479     *
480     * @return the hash code value.
481     */
482    @Override
483    public int hashCode() {
484        return 31*super.hashCode() + Objects.hash(text, xhat, yhat, location, height);
485    }
486
487    /**
488     * Holds the default XML representation for this object.
489     */
490    @SuppressWarnings("FieldNameHidesFieldInSuperclass")
491    protected static final XMLFormat<ModelNote> XML = new XMLFormat<ModelNote>(ModelNote.class) {
492
493        @Override
494        public ModelNote newInstance(Class<ModelNote> cls, XMLFormat.InputElement xml) throws XMLStreamException {
495            return FACTORY.object();
496        }
497
498        @Override
499        public void read(XMLFormat.InputElement xml, ModelNote obj) throws XMLStreamException {
500            //  Read in the font information.
501            String fontStr = xml.getAttribute("font", DEFAULT_FONT_CODE);
502            Font font = Font.decode(fontStr);
503
504            GenModelNote.XML.read(xml, obj);     // Call parent read.
505
506            //  Read in the text string.
507            String text = xml.get("Note", String.class);
508
509            //  Read in the height.
510            Parameter<Length> height = xml.get("Height", Parameter.class);
511
512            //  Read in the location & orientation of the note in model space.
513            Vector<Dimensionless> xhat = xml.get("XHat", Vector.class);
514            Vector<Dimensionless> yhat = xml.get("YHat", Vector.class);
515            Point location = xml.get("Location", Point.class);
516
517            // Make sure the physical dimensions and units are consistent.
518            Unit<Length> unit = height.getUnit();
519            int dim = GeomUtil.maxPhyDimension(xhat, yhat, location);
520            if (dim < 2)
521                throw new XMLStreamException(
522                        MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "ModelNote", dim));
523            xhat = xhat.toDimension(dim);
524            yhat = yhat.toDimension(dim);
525            location = location.toDimension(dim).to(unit);
526
527            //  Fill in the object definition.
528            obj.text = text;
529            obj.font = font;
530            obj.xhat = xhat;
531            obj.yhat = yhat;
532            obj.location = location;
533            obj.height = height;
534        }
535
536        @Override
537        public void write(ModelNote obj, XMLFormat.OutputElement xml) throws XMLStreamException {
538            //  Write out a font string.
539            Font font = obj.getFont();
540            String fontStr = encodeFont(font.getName(), font.getStyle(), font.getSize());
541            xml.setAttribute("font", fontStr);
542
543            GenModelNote.XML.write(obj, xml);    // Call parent write.
544
545            //  Write out the text string.
546            xml.add(obj.getNote(), "Note", String.class);
547
548            //  Write out note height.
549            xml.add(obj.getHeight(), "Height", Parameter.class);
550
551            //  Write out the location & orientation of the note in model space.
552            xml.add(obj.getXHat(), "XHat", Vector.class);
553            xml.add(obj.getYHat(), "YHat", Vector.class);
554            xml.add(obj.getLocation(), "Location", Point.class);
555        }
556    };
557
558    ///////////////////////
559    // Factory creation. //
560    ///////////////////////
561    private ModelNote() { }
562
563    @SuppressWarnings("unchecked")
564    private static ModelNote copyOf(ModelNote original) {
565        ModelNote obj = FACTORY.object();
566        obj.text = original.text;
567        obj.xhat = original.xhat.copy();
568        obj.yhat = original.yhat.copy();
569        obj.location = original.location.copy();
570        obj.height = original.height.copy();
571        obj.font = original.font;
572        original.copyState(obj);
573        return obj;
574    }
575
576    @SuppressWarnings("unchecked")
577    private static final ObjectFactory<ModelNote> FACTORY = new ObjectFactory<ModelNote>() {
578        @Override
579        protected ModelNote create() {
580            return new ModelNote();
581        }
582
583        @Override
584        protected void cleanup(ModelNote obj) {
585            obj.reset();
586            obj.text = null;
587            obj.xhat = null;
588            obj.yhat = null;
589            obj.location = null;
590            obj.height = null;
591            obj.font = null;
592        }
593    };
594
595}