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