1 /*
  2     Copyright 2008-2024
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true*/
 33 /*jslint nomen: true, plusplus: true*/
 34 
 35 /**
 36  * @fileoverview Geometry objects for measurements are defined in this file. This file stores all
 37  * style and functional properties that are required to use a tape measure on
 38  * a board.
 39  */
 40 
 41 import JXG from "../jxg.js";
 42 import Type from "../utils/type.js";
 43 import GeometryElement from "../base/element.js";
 44 import Prefix from "../parser/prefix.js";
 45 
 46 /**
 47  * @class A tape measure can be used to measure distances between points.
 48  * <p>
 49  * The two defining points of the tape measure (which is a segment) do not inherit by default the attribute "visible" from
 50  * the segment. Otherwise the tape meassure would be inaccessible if the two points coincide and the segment is hidden.
 51  *
 52  * @pseudo
 53  * @name Tapemeasure
 54  * @augments Segment
 55  * @constructor
 56  * @type JXG.Segment
 57  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
 58  * @param {Array_Array} start,end, The two arrays give the initial position where the tape measure
 59  * is drawn on the board.
 60  * @example
 61  * // Create a tape measure
 62  * var p1 = board.create('point', [0,0]);
 63  * var p2 = board.create('point', [1,1]);
 64  * var p3 = board.create('point', [3,1]);
 65  * var tape = board.create('tapemeasure', [[1, 2], [4, 2]], {name:'dist'});
 66  * </pre><div class="jxgbox" id="JXG6d9a2cda-22fe-4cd1-9d94-34283b1bdc01" style="width: 200px; height: 200px;"></div>
 67  * <script type="text/javascript">
 68  *   (function () {
 69  *     var board = JXG.JSXGraph.initBoard('JXG6d9a2cda-22fe-4cd1-9d94-34283b1bdc01', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false});
 70  *     var p1 = board.create('point', [0,0]);
 71  *     var p2 = board.create('point', [1,1]);
 72  *     var p3 = board.create('point', [3,1]);
 73  *     var tape = board.create('tapemeasure', [[1, 2], [4, 2]], {name:'dist'} );
 74  *   })();
 75  * </script><pre>
 76  */
 77 JXG.createTapemeasure = function (board, parents, attributes) {
 78     var pos0, pos1, attr, withTicks, withText, digits, li, p1, p2, n, ti;
 79 
 80     pos0 = parents[0];
 81     pos1 = parents[1];
 82 
 83     // start point
 84     attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "point1");
 85     p1 = board.create("point", pos0, attr);
 86 
 87     // end point
 88     attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "point2");
 89     p2 = board.create("point", pos1, attr);
 90 
 91     p1.setAttribute({ ignoredSnapToPoints: [p2.id] });
 92     p2.setAttribute({ ignoredSnapToPoints: [p1.id] });
 93 
 94     // tape measure line
 95     attr = Type.copyAttributes(attributes, board.options, "tapemeasure");
 96     withTicks = attr.withticks;
 97     withText = attr.withlabel;
 98     digits = attr.digits;
 99 
100     if (digits === 2 && attr.precision !== 2) {
101         // Backward compatibility
102         digits = attr.precision;
103     }
104 
105     // Below, we will replace the label by the measurement function.
106     if (withText) {
107         attr.withlabel = true;
108     }
109     li = board.create("segment", [p1, p2], attr);
110     // p1, p2 are already added to li.inherits
111 
112     if (withText) {
113         if (attributes.name && attributes.name !== "") {
114             n = attributes.name + " = ";
115         } else {
116             n = "";
117         }
118         li.label.setText(function () {
119             var digits = Type.evaluate(li.label.visProp.digits);
120 
121             if (li.label.useLocale()) {
122                 return n + li.label.formatNumberLocale(p1.Dist(p2), digits);
123             }
124             return n + Type.toFixed(p1.Dist(p2), digits);
125         });
126     }
127 
128     if (withTicks) {
129         attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "ticks");
130         //ticks  = 2;
131         ti = board.create("ticks", [li, 0.1], attr);
132         li.inherits.push(ti);
133     }
134 
135     // override the segments's remove method to ensure the removal of all elements
136     /** @ignore */
137     li.remove = function () {
138         if (withTicks) {
139             li.removeTicks(ti);
140         }
141 
142         board.removeObject(p2);
143         board.removeObject(p1);
144 
145         GeometryElement.prototype.remove.call(this);
146     };
147 
148     /**
149      * Returns the length of the tape measure.
150      * @name Value
151      * @memberOf Tapemeasure.prototype
152      * @function
153      * @returns {Number} length of tape measure.
154      */
155     li.Value = function () {
156         return p1.Dist(p2);
157     };
158 
159     p1.dump = false;
160     p2.dump = false;
161 
162     li.elType = "tapemeasure";
163     li.getParents = function () {
164         return [
165             [p1.X(), p1.Y()],
166             [p2.X(), p2.Y()]
167         ];
168     };
169 
170     li.subs = {
171         point1: p1,
172         point2: p2
173     };
174 
175     if (withTicks) {
176         ti.dump = false;
177     }
178 
179     li.methodMap = JXG.deepCopy(li.methodMap, {
180         Value: "Value"
181     });
182 
183     li.prepareUpdate().update();
184     if (!board.isSuspendedUpdate) {
185         li.updateVisibility().updateRenderer();
186         // The point updates are necessary in case of snapToGrid==true
187         li.point1.updateVisibility().updateRenderer();
188         li.point2.updateVisibility().updateRenderer();
189     }
190 
191     return li;
192 };
193 
194 JXG.registerElement("tapemeasure", JXG.createTapemeasure);
195 
196 /**
197  * @class Measurement element. Under the hood this is a text element which has a method Value. The text to be displayed
198  * is the result of the evaluation of a prefix expression, see {@link JXG.PrefixParser}.
199  * <p>
200  * The purpose of this element is to display values of measurements of geometric objects, like the radius of a circle,
201  * as well as expressions consisting of measurements.
202  *
203  * @pseudo
204  * @name Measurement
205  * @augments Text
206  * @constructor
207  * @type JXG.Text
208  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
209  * @param {Point|Array_Point|Array_Array} x,y,expression
210  * Here, expression is a prefix expression, see {@link JXG.PrefixParser}.
211  * @example
212  * var p1 = board.create('point', [1, 1]);
213  * var p2 = board.create('point', [1, 3]);
214  * var ci1 = board.create('circle', [p1, p2]);
215  *
216  * var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
217  *     visible: true,
218  *     prefix: 'area: ',
219  *     baseUnit: 'cm'
220  * });
221  *
222  * var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
223  *     prefix: 'radius: ',
224  *     baseUnit: 'cm'
225  * });
226  *
227  * </pre><div id="JXG6359237a-79bc-4689-92fc-38d3ebeb769d" class="jxgbox" style="width: 300px; height: 300px;"></div>
228  * <script type="text/javascript">
229  *     (function() {
230  *         var board = JXG.JSXGraph.initBoard('JXG6359237a-79bc-4689-92fc-38d3ebeb769d',
231  *             {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright: false, shownavigation: false});
232  *     var p1 = board.create('point', [1, 1]);
233  *     var p2 = board.create('point', [1, 3]);
234  *     var ci1 = board.create('circle', [p1, p2]);
235  *
236  *     var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
237  *         visible: true,
238  *         prefix: 'area: ',
239  *         baseUnit: 'cm'
240  *     });
241  *
242  *     var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
243  *         prefix: 'radius: ',
244  *         baseUnit: 'cm'
245  *     });
246  *
247  *     })();
248  *
249  * </script><pre>
250  *
251  * @example
252  * var p1 = board.create('point', [1, 1]);
253  * var p2 = board.create('point', [1, 3]);
254  * var ci1 = board.create('circle', [p1, p2]);
255  * var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
256  * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
257  *
258  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
259  *     prefix: 'm1: ',
260  *     baseUnit: 'cm'
261  * });
262  *
263  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
264  *     prefix: 'm2: ',
265  *     baseUnit: 'cm'
266  * });
267  *
268  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
269  *     prefix: 'm3: ',
270  *     baseUnit: 'cm',
271  *     dim: 1
272  * });
273  *
274  * var m4 = board.create('measurement', [2, -6,
275  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
276  *     ], {
277  *     prefix: 'm4: ',
278  *     baseUnit: 'cm'
279  * });
280  *
281  * </pre><div id="JXG49903663-6450-401e-b0d9-f025a6677d4a" class="jxgbox" style="width: 300px; height: 300px;"></div>
282  * <script type="text/javascript">
283  *     (function() {
284  *         var board = JXG.JSXGraph.initBoard('JXG49903663-6450-401e-b0d9-f025a6677d4a',
285  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
286  *     var p1 = board.create('point', [1, 1]);
287  *     var p2 = board.create('point', [1, 3]);
288  *     var ci1 = board.create('circle', [p1, p2]);
289  *     var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
290  *     var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
291  *
292  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
293  *     prefix: 'm1: ',
294  *     baseUnit: 'cm'
295  * });
296  *
297  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
298  *     prefix: 'm2: ',
299  *     baseUnit: 'cm'
300  * });
301  *
302  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
303  *     prefix: 'm3: ',
304  *     baseUnit: 'cm',
305  *     dim: 1
306  * });
307  *
308  * var m4 = board.create('measurement', [2, -6,
309  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
310  *     ], {
311  *     prefix: 'm4: ',
312  *     baseUnit: 'cm'
313  * });
314  *
315  *     })();
316  *
317  * </script><pre>
318  *
319  */
320 JXG.createMeasurement = function (board, parents, attributes) {
321     var el, attr,
322         x, y, term,
323         i;
324 
325     attr = Type.copyAttributes(attributes, board.options, "measurement");
326 
327     x = parents[0];
328     y = parents[1];
329     term = parents[2];
330 
331     el = board.create("text", [x, y, ''], attr);
332     el.type = Type.OBJECT_TYPE_MEASUREMENT;
333     el.elType = 'measurement';
334 
335     el.Value = function () {
336         return Prefix.parse(term, 'execute');
337     };
338 
339     el.Dimension = function () {
340         var d = Type.evaluate(el.visProp.dim);
341 
342         if (d !== null) {
343             return d;
344         }
345         return Prefix.dimension(term);
346     };
347 
348     el.Unit = function () {
349         var unit = '',
350             units = Type.evaluate(el.visProp.units),
351             dim = el.Dimension();
352 
353         if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) {
354             unit = Type.evaluate(units[dim]);
355         } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) {
356             // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'.
357             unit = Type.evaluate(units['dim' + dim]);
358         } else {
359             unit = Type.evaluate(el.visProp.baseunit);
360 
361             if (dim === 0) {
362                 unit = '';
363             } else if (dim > 1 && unit !== '') {
364                 unit = unit + '^{' + dim + '}';
365             }
366         }
367 
368         return unit;
369     };
370 
371     el.getTerm = function () {
372         return term;
373     };
374 
375     el.getMethod = function () {
376         var method = term[0];
377         if (method === "V") {
378             method = "Value";
379         }
380         return method;
381     };
382 
383     el.toPrefix = function () {
384         return Prefix.toPrefix(term);
385     };
386 
387     el.getParents = function () {
388         return Prefix.getParents(term);
389     };
390     el.addParents(el.getParents());
391     for (i = 0; i < el.parents.length; i++) {
392         board.select(el.parents[i]).addChild(el);
393     }
394 
395     /**
396      * @class
397      * @ignore
398      */
399     el.setText(function () {
400         var prefix = '',
401             suffix = '',
402             dim = el.Dimension(),
403             digits = Type.evaluate(el.visProp.digits),
404             unit = el.Unit(),
405             val = el.Value(),
406             i;
407 
408         if (Type.evaluate(el.visProp.showprefix)) {
409             prefix = el.visProp.formatprefix.apply(el, [Type.evaluate(el.visProp.prefix)]);
410         }
411         if (Type.evaluate(el.visProp.showsuffix)) {
412             suffix = el.visProp.formatsuffix.apply(el, [Type.evaluate(el.visProp.suffix)]);
413         }
414 
415         if (Type.isNumber(val)) {
416             if (digits === 'none') {
417                 // do nothing
418             } else if (digits === 'auto') {
419                 if (el.useLocale()) {
420                     val = el.formatNumberLocale(val);
421                 } else {
422                     val = Type.autoDigits(val);
423                 }
424             } else {
425                 if (el.useLocale()) {
426                     val = el.formatNumberLocale(val, digits);
427                 } else {
428                     val = Type.toFixed(val, digits);
429                 }
430             }
431         } else if (Type.isArray(val)) {
432             for (i = 0; i < val.length; i++) {
433                 if (!Type.isNumber(val[i])) {
434                     continue;
435                 }
436                 if (digits === 'none') {
437                     // do nothing
438                 } else if (digits === 'auto') {
439                     if (el.useLocale()) {
440                         val[i] = el.formatNumberLocale(val[i]);
441                     } else {
442                         val[i] = Type.autoDigits(val[i]);
443                     }
444                 } else {
445                     if (el.useLocale()) {
446                         val[i] = el.formatNumberLocale(val[i], digits);
447                     } else {
448                         val[i] = Type.toFixed(val[i], digits);
449                     }
450                 }
451             }
452         }
453 
454         if (dim === 'coords' && Type.isArray(val)) {
455             if (val.length === 2) {
456                 val.unshift(undefined);
457             }
458             val = el.visProp.formatcoords.apply(el, [val[1], val[2], val[0]]);
459         }
460 
461         if (dim === 'direction' && Type.isArray(val)) {
462             val = el.visProp.formatdirection.apply(el, [val[0], val[1]]);
463         }
464 
465         if (Type.isString(dim)) {
466             return prefix + val + suffix;
467         }
468 
469         if (isNaN(dim)) {
470             return prefix + 'NaN' + suffix;
471         }
472 
473         return prefix + val + unit + suffix;
474     });
475 
476     el.methodMap = Type.deepCopy(el.methodMap, {
477         Value: "Value",
478         Dimension: "Dimension",
479         Unit: "Unit",
480         getTerm: "getTerm",
481         Term: "getTerm",
482         getMethod: "getMethod",
483         Method: "getMethod",
484         getParents: "getParents",
485         Parents: "getParents"
486     });
487 
488     return el;
489 };
490 
491 JXG.registerElement("measurement", JXG.createMeasurement);
492