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 = li.label.evalVisProp('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 Display measurements of geometric elements and the arithmetic operations of measurements.
198  * Under the hood this is a text element which has a method Value. The text to be displayed
199  * is the result of the evaluation of a prefix expression, see {@link JXG.PrefixParser}.
200  * <p>
201  * The purpose of this element is to display values of measurements of geometric objects, like the radius of a circle,
202  * as well as expressions consisting of measurements.
203  *
204  * @pseudo
205  * @name Measurement
206  * @augments Text
207  * @constructor
208  * @type JXG.Text
209  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
210  * @param {Point|Array_Point|Array_Array} x,y,expression
211  * Here, expression is a prefix expression, see {@link JXG.PrefixParser}.
212  * @example
213  * var p1 = board.create('point', [1, 1]);
214  * var p2 = board.create('point', [1, 3]);
215  * var ci1 = board.create('circle', [p1, p2]);
216  *
217  * var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
218  *     visible: true,
219  *     prefix: 'area: ',
220  *     baseUnit: 'cm'
221  * });
222  *
223  * var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
224  *     prefix: 'radius: ',
225  *     baseUnit: 'cm'
226  * });
227  *
228  * </pre><div id="JXG6359237a-79bc-4689-92fc-38d3ebeb769d" class="jxgbox" style="width: 300px; height: 300px;"></div>
229  * <script type="text/javascript">
230  *     (function() {
231  *         var board = JXG.JSXGraph.initBoard('JXG6359237a-79bc-4689-92fc-38d3ebeb769d',
232  *             {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright: false, shownavigation: false});
233  *     var p1 = board.create('point', [1, 1]);
234  *     var p2 = board.create('point', [1, 3]);
235  *     var ci1 = board.create('circle', [p1, p2]);
236  *
237  *     var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
238  *         visible: true,
239  *         prefix: 'area: ',
240  *         baseUnit: 'cm'
241  *     });
242  *
243  *     var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
244  *         prefix: 'radius: ',
245  *         baseUnit: 'cm'
246  *     });
247  *
248  *     })();
249  *
250  * </script><pre>
251  *
252  * @example
253  * var p1 = board.create('point', [1, 1]);
254  * var p2 = board.create('point', [1, 3]);
255  * var ci1 = board.create('circle', [p1, p2]);
256  * var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
257  * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
258  *
259  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
260  *     prefix: 'm1: ',
261  *     baseUnit: 'cm'
262  * });
263  *
264  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
265  *     prefix: 'm2: ',
266  *     baseUnit: 'cm'
267  * });
268  *
269  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
270  *     prefix: 'm3: ',
271  *     baseUnit: 'cm',
272  *     dim: 1
273  * });
274  *
275  * var m4 = board.create('measurement', [2, -6,
276  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
277  *     ], {
278  *     prefix: 'm4: ',
279  *     baseUnit: 'cm'
280  * });
281  *
282  * </pre><div id="JXG49903663-6450-401e-b0d9-f025a6677d4a" class="jxgbox" style="width: 300px; height: 300px;"></div>
283  * <script type="text/javascript">
284  *     (function() {
285  *         var board = JXG.JSXGraph.initBoard('JXG49903663-6450-401e-b0d9-f025a6677d4a',
286  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
287  *     var p1 = board.create('point', [1, 1]);
288  *     var p2 = board.create('point', [1, 3]);
289  *     var ci1 = board.create('circle', [p1, p2]);
290  *     var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
291  *     var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
292  *
293  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
294  *     prefix: 'm1: ',
295  *     baseUnit: 'cm'
296  * });
297  *
298  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
299  *     prefix: 'm2: ',
300  *     baseUnit: 'cm'
301  * });
302  *
303  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
304  *     prefix: 'm3: ',
305  *     baseUnit: 'cm',
306  *     dim: 1
307  * });
308  *
309  * var m4 = board.create('measurement', [2, -6,
310  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
311  *     ], {
312  *     prefix: 'm4: ',
313  *     baseUnit: 'cm'
314  * });
315  *
316  *     })();
317  *
318  * </script><pre>
319  *
320  */
321 JXG.createMeasurement = function (board, parents, attributes) {
322     var el, attr,
323         x, y, term,
324         i;
325 
326     attr = Type.copyAttributes(attributes, board.options, "measurement");
327 
328     x = parents[0];
329     y = parents[1];
330     term = parents[2];
331 
332     el = board.create("text", [x, y, ''], attr);
333     el.type = Type.OBJECT_TYPE_MEASUREMENT;
334     el.elType = 'measurement';
335 
336     el.Value = function () {
337         return Prefix.parse(term, 'execute');
338     };
339 
340     el.Dimension = function () {
341         var d = el.evalVisProp('dim');
342 
343         if (d !== null) {
344             return d;
345         }
346         return Prefix.dimension(term);
347     };
348 
349     el.Unit = function (dimension) {
350         var unit = '',
351             units = el.evalVisProp('units'),
352             dim = dimension ?? el.Dimension();
353 
354         if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) {
355             unit = el.eval(units[dim]);
356         } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) {
357             // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'.
358             unit = el.eval(units['dim' + dim]);
359         } else {
360             unit = el.evalVisProp('baseunit');
361 
362             if (dim === 0) {
363                 unit = '';
364             } else if (dim > 1 && unit !== '') {
365                 unit = unit + '^{' + dim + '}';
366             }
367         }
368 
369         return unit;
370     };
371 
372     el.getTerm = function () {
373         return term;
374     };
375 
376     el.getMethod = function () {
377         var method = term[0];
378         if (method === "V") {
379             method = "Value";
380         }
381         return method;
382     };
383 
384     el.toPrefix = function () {
385         return Prefix.toPrefix(term);
386     };
387 
388     el.getParents = function () {
389         return Prefix.getParents(term);
390     };
391     el.addParents(el.getParents());
392     for (i = 0; i < el.parents.length; i++) {
393         board.select(el.parents[i]).addChild(el);
394     }
395 
396     /**
397      * @class
398      * @ignore
399      */
400     el.setText(function () {
401         var prefix = '',
402             suffix = '',
403             dim = el.Dimension(),
404             digits = el.evalVisProp('digits'),
405             unit = el.Unit(),
406             val = el.Value(),
407             i;
408 
409         if (el.evalVisProp('showprefix')) {
410             prefix = el.evalVisProp('prefix');
411         }
412         if (el.evalVisProp('showsuffix')) {
413             suffix = el.evalVisProp('suffix');
414         }
415 
416         if (Type.isNumber(val)) {
417             if (digits === 'none') {
418                 // do nothing
419             } else if (digits === 'auto') {
420                 if (el.useLocale()) {
421                     val = el.formatNumberLocale(val);
422                 } else {
423                     val = Type.autoDigits(val);
424                 }
425             } else {
426                 if (el.useLocale()) {
427                     val = el.formatNumberLocale(val, digits);
428                 } else {
429                     val = Type.toFixed(val, digits);
430                 }
431             }
432         } else if (Type.isArray(val)) {
433             for (i = 0; i < val.length; i++) {
434                 if (!Type.isNumber(val[i])) {
435                     continue;
436                 }
437                 if (digits === 'none') {
438                     // do nothing
439                 } else if (digits === 'auto') {
440                     if (el.useLocale()) {
441                         val[i] = el.formatNumberLocale(val[i]);
442                     } else {
443                         val[i] = Type.autoDigits(val[i]);
444                     }
445                 } else {
446                     if (el.useLocale()) {
447                         val[i] = el.formatNumberLocale(val[i], digits);
448                     } else {
449                         val[i] = Type.toFixed(val[i], digits);
450                     }
451                 }
452             }
453         }
454 
455         if (dim === 'coords' && Type.isArray(val)) {
456             if (val.length === 2) {
457                 val.unshift(undefined);
458             }
459             val = el.visProp.formatcoords(el, val[1], val[2], val[0]);
460         }
461 
462         if (dim === 'direction' && Type.isArray(val)) {
463             val = el.visProp.formatdirection(el, val[0], val[1]);
464         }
465 
466         if (Type.isString(dim)) {
467             return prefix + val + suffix;
468         }
469 
470         if (isNaN(dim)) {
471             return prefix + 'NaN' + suffix;
472         }
473 
474         return prefix + val + unit + suffix;
475     });
476 
477     el.methodMap = Type.deepCopy(el.methodMap, {
478         Value: "Value",
479         Dimension: "Dimension",
480         Unit: "Unit",
481         getTerm: "getTerm",
482         Term: "getTerm",
483         getMethod: "getMethod",
484         Method: "getMethod",
485         getParents: "getParents",
486         Parents: "getParents"
487     });
488 
489     return el;
490 };
491 
492 JXG.registerElement("measurement", JXG.createMeasurement);
493