1 /*
  2     Copyright 2008-2022
  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 <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true, window: true*/
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /* depends:
 37  jxg
 38  base/constants
 39  base/coords
 40  base/element
 41  math/math
 42  utils/type
 43  */
 44 
 45 /**
 46  * @fileoverview In this file the ForeignObject element is defined.
 47  */
 48 
 49  define([
 50     'jxg', 'base/constants', 'base/coords', 'base/element', 'math/math', 'utils/type', 'base/coordselement'
 51 ], function (JXG, Const, Coords, GeometryElement, Mat, Type, CoordsElement) {
 52 
 53     "use strict";
 54 
 55     /**
 56      * Construct and handle SVG foreignObjects.
 57      *
 58      * @class Creates a new foreignObject object. Do not use this constructor to create a foreignObject. Use {@link JXG.Board#create} with
 59      * type {@link foreignobject} instead.
 60      * @augments JXG.GeometryElement
 61      * @augments JXG.CoordsElement
 62      * @param {string|JXG.Board} board The board the new foreignObject is drawn on.
 63      * @param {Array} coordinates An array with the user coordinates of the foreignObject.
 64      * @param {Object} attributes An object containing visual and - optionally - a name and an id.
 65      * @param {string|function} url An URL string or a function returning an URL string.
 66      * @param  {Array} size Array containing width and height of the foreignObject in user coordinates.
 67      *
 68      */
 69      JXG.ForeignObject = function (board, coords, attributes, content, size) {
 70         this.constructor(board, attributes, Const.OBJECT_TYPE_FOREIGNOBJECT, Const.OBJECT_CLASS_OTHER);
 71         this.element = this.board.select(attributes.anchor);
 72         this.coordsConstructor(coords);
 73 
 74         this._useUserSize = false;
 75 
 76         /**
 77          * Array of length two containing [width, height] of the foreignObject in pixel.
 78          * @type Array
 79          */
 80         this.size = [1, 1];
 81         if (Type.exists(size) && size.length > 0) {
 82             this._useUserSize = true;
 83 
 84             this.W = Type.createFunction(size[0], this.board, '');
 85             this.H = Type.createFunction(size[1], this.board, '');
 86             this.usrSize = [this.W(), this.H()];
 87         }
 88 
 89         // this.size = [Math.abs(this.usrSize[0] * board.unitX), Math.abs(this.usrSize[1] * board.unitY)];
 90 
 91         /**
 92          * 'href' of the foreignObject.
 93          * @type {string}
 94          */
 95         this.content = content;
 96 
 97         this.elType = 'foreignobject';
 98 
 99         // span contains the anchor point and the two vectors
100         // spanning the foreignObject rectangle.
101         // this.span = [
102         //     this.coords.usrCoords.slice(0),
103         //     [this.coords.usrCoords[0], this.W(), 0],
104         //     [this.coords.usrCoords[0], 0, this.H()]
105         // ];
106         //this.parent = board.select(attributes.anchor);
107 
108         this.id = this.board.setId(this, 'Im');
109 
110         this.board.renderer.drawForeignObject(this);
111         this.board.finalizeAdding(this);
112 
113         this.methodMap = JXG.deepCopy(this.methodMap, {
114             addTransformation: 'addTransform',
115             trans: 'addTransform'
116         });
117     };
118 
119     JXG.ForeignObject.prototype = new GeometryElement();
120     Type.copyPrototypeMethods(JXG.ForeignObject, CoordsElement, 'coordsConstructor');
121 
122     JXG.extend(JXG.ForeignObject.prototype, /** @lends JXG.ForeignObject.prototype */ {
123 
124         /**
125          * Checks whether (x,y) is over or near the image;
126          * @param {Number} x Coordinate in x direction, screen coordinates.
127          * @param {Number} y Coordinate in y direction, screen coordinates.
128          * @returns {Boolean} True if (x,y) is over the image, False otherwise.
129          */
130         hasPoint: function (x, y) {
131             var dx, dy, r, type, prec,
132                 c, v, p, dot,
133                 len = this.transformations.length;
134 
135                 if (Type.isObject(Type.evaluate(this.visProp.precision))) {
136                     type = this.board._inputDevice;
137                     prec = Type.evaluate(this.visProp.precision[type]);
138                 } else {
139                     // 'inherit'
140                     prec = this.board.options.precision.hasPoint;
141                 }
142 
143             // Easy case: no transformation
144             if (len === 0) {
145                 dx = x - this.coords.scrCoords[1];
146                 dy = this.coords.scrCoords[2] - y;
147                 r = prec;
148 
149                 return dx >= -r && dx - this.size[0] <= r &&
150                        dy >= -r && dy - this.size[1] <= r;
151             }
152 
153             // foreignObject is transformed
154             c = new Coords(Const.COORDS_BY_SCREEN, [x, y], this.board);
155             // v is the vector from anchor point to the drag point
156             c = c.usrCoords;
157             v = [c[0] - this.span[0][0],
158                 c[1] - this.span[0][1],
159                 c[2] - this.span[0][2]];
160             dot = Mat.innerProduct;   // shortcut
161 
162             // Project the drag point to the sides.
163             p = dot(v, this.span[1]);
164             if (0 <= p && p <= dot(this.span[1], this.span[1])) {
165                 p = dot(v, this.span[2]);
166 
167                 if (0 <= p && p <= dot(this.span[2], this.span[2])) {
168                     return true;
169                 }
170             }
171             return false;
172         },
173 
174         /**
175          * Recalculate the coordinates of lower left corner and the width and height.
176          *
177          * @returns {JXG.ForeignObject} A reference to the element
178          * @private
179          */
180         update: function (fromParent) {
181             if (!this.needsUpdate) {
182                 return this;
183             }
184             this.updateCoords(fromParent);
185             this.updateSize();
186             // this.updateSpan();
187             return this;
188         },
189 
190         /**
191          * Send an update request to the renderer.
192          * @private
193          */
194         updateRenderer: function () {
195             return this.updateRendererGeneric('updateForeignObject');
196         },
197 
198         /**
199          * Updates the internal arrays containing size of the foreignObject.
200          * @returns {JXG.ForeignObject} A reference to the element
201          * @private
202          */
203         updateSize: function () {
204             var bb = [0, 0];
205 
206             if (this._useUserSize) {
207                 this.usrSize = [this.W(), this.H()];
208                 this.size = [Math.abs(this.usrSize[0] * this.board.unitX),
209                              Math.abs(this.usrSize[1] * this.board.unitY)];
210             } else {
211                 if (this.rendNode.hasChildNodes()) {
212                     bb = this.rendNode.childNodes[0].getBoundingClientRect();
213                     this.size = [bb.width, bb.height];
214                 }
215             }
216 
217             return this;
218         },
219 
220         /**
221          * Update the anchor point of the foreignObject, i.e. the lower left corner
222          * and the two vectors which span the rectangle.
223          * @returns {JXG.ForeignObject} A reference to the element
224          * @private
225          *
226          */
227         updateSpan: function () {
228             var i, j, len = this.transformations.length, v = [];
229 
230             if (len === 0) {
231                 this.span = [[this.Z(), this.X(), this.Y()],
232                     [this.Z(), this.W(), 0],
233                     [this.Z(), 0, this.H()]];
234             } else {
235                 // v contains the three defining corners of the rectangle/image
236                 v[0] = [this.Z(), this.X(), this.Y()];
237                 v[1] = [this.Z(), this.X() + this.W(), this.Y()];
238                 v[2] = [this.Z(), this.X(), this.Y() + this.H()];
239 
240                 // Transform the three corners
241                 for (i = 0; i < len; i++) {
242                     for (j = 0; j < 3; j++) {
243                         v[j] = Mat.matVecMult(this.transformations[i].matrix, v[j]);
244                     }
245                 }
246                 // Normalize the vectors
247                 for (j = 0; j < 3; j++) {
248                     v[j][1] /= v[j][0];
249                     v[j][2] /= v[j][0];
250                     v[j][0] /= v[j][0];
251                 }
252                 // Compute the two vectors spanning the rectangle
253                 // by subtracting the anchor point.
254                 for (j = 1; j < 3; j++) {
255                     v[j][0] -= v[0][0];
256                     v[j][1] -= v[0][1];
257                     v[j][2] -= v[0][2];
258                 }
259                 this.span = v;
260             }
261 
262             return this;
263         },
264 
265         addTransform: function (transform) {
266             var i;
267 
268             if (Type.isArray(transform)) {
269                 for (i = 0; i < transform.length; i++) {
270                     this.transformations.push(transform[i]);
271                 }
272             } else {
273                 this.transformations.push(transform);
274             }
275 
276             return this;
277         },
278 
279         // Documented in element.js
280         getParents: function () {
281             var p = [this.url, [this.Z(), this.X(), this.Y()], this.usrSize];
282 
283             if (this.parents.length !== 0) {
284                 p = this.parents;
285             }
286 
287             return p;
288         },
289 
290         /**
291          * Set the width and height of the foreignObject. After setting a new size,
292          * board.update() or foreignobject.fullUpdate()
293          * has to be called to make the change visible.
294          * @param  {number, function, string} width  Number, function or string
295          *                            that determines the new width of the foreignObject
296          * @param  {number, function, string} height Number, function or string
297          *                            that determines the new height of the foreignObject
298          * @returns {JXG.ForeignObject} A reference to the element
299          *
300          */
301         setSize: function(width, height) {
302             this.W = Type.createFunction(width, this.board, '');
303             this.H = Type.createFunction(height, this.board, '');
304             this._useUserSize = true;
305 
306             return this;
307         },
308 
309         /**
310          * Returns the width of the foreignObject in user coordinates.
311          * @returns {number} width of the image in user coordinates
312          */
313         W: function() {},  // Needed for docs, defined in constructor
314 
315         /**
316          * Returns the height of the foreignObject in user coordinates.
317          * @returns {number} height of the image in user coordinates
318          */
319         H: function() {}  // Needed for docs, defined in constructor
320     });
321 
322     /**
323      * @class This element is used to provide a constructor for arbitrary content in
324      * an SVG foreignObject container.
325      * <p>
326      * Instead of board.create('foreignobject') the shortcut board.create('fo') may be used.
327      * 
328      * <p style="background-color:#dddddd; padding:10px"><b>NOTE:</b> In Safari up to version 15, a foreignObject does not obey the layer structure
329      * if it contains <video> or <iframe> tags, as well as elements which are 
330      * positioned with <tt>position:absolute|relative|fixed</tt>. In this  case, the foreignobject will be 
331      * "above" the JSXGraph construction.
332      * </p>
333      * 
334      * @pseudo
335      * @description
336      * @name ForeignObject
337      * @augments JXG.ForeignObject
338      * @constructor
339      * @type JXG.ForeignObject
340      *
341      * @param {String} content HTML content of the foreignObject. May also be <video> or <iframe>
342      * @param {Array} position Position of the foreignObject given by [x, y] in user coordinates. Same as for images.
343      * @param {Array} [size] (Optional) argument size of the foreignObject in user coordinates. If not given, size is specified by the HTML attributes
344      * or CSS properties of the content.
345      *
346      * @see Image
347      *
348      * @example
349      * var p = board.create('point', [1, 7], {size: 16});
350      * var fo = board.create('foreignobject', [
351      *     '<video width="300" height="200" src="https://eucbeniki.sio.si/vega2/278/Video_metanje_oge_.mp4" type="html5video" controls>',
352      *     [0, -3], [9, 6]],
353      *     {layer: 8, fixed: true}
354      *  );
355      *
356      * </pre><div id="JXG0c122f2c-3671-4a28-80a9-f4c523eeda89" class="jxgbox" style="width: 500px; height: 500px;"></div>
357      * <script type="text/javascript">
358      *     (function() {
359      *         var board = JXG.JSXGraph.initBoard('JXG0c122f2c-3671-4a28-80a9-f4c523eeda89',
360      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
361      *     var p = board.create('point', [1, 7], {size: 16});
362      *     var fo = board.create('foreignobject', [
363      *         '<video width="300" height="200" src="https://eucbeniki.sio.si/vega2/278/Video_metanje_oge_.mp4" type="html5video" controls>',
364      *         [0, -3], [9, 6]],
365      *         {layer: 8, fixed: true}
366      *      );
367      *
368      *     })();
369      *
370      * </script><pre>
371      *
372      * @example
373      * var p = board.create('point', [1, 7], {size: 16});
374      * var fo = board.create('fo', [
375      *     '<div style="background-color:blue; color: yellow; padding:20px; width:200px; height:50px; ">Hello</div>',
376      *     [-7, -6]],
377      *     {layer: 1, fixed: false}
378      *  );
379      *
380      * </pre><div id="JXG1759c868-1a4a-4767-802c-91f84902e3ec" class="jxgbox" style="width: 500px; height: 500px;"></div>
381      * <script type="text/javascript">
382      *     (function() {
383      *         var board = JXG.JSXGraph.initBoard('JXG1759c868-1a4a-4767-802c-91f84902e3ec',
384      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
385      *     var p = board.create('point', [1, 7], {size: 16});
386      *     var fo = board.create('foreignobject', [
387      *         '<div style="background-color:blue; color: yellow; padding:20px; width:200px; height:50px; ">Hello</div>',
388      *         [-7, -6]],
389      *         {layer: 1, fixed: false}
390      *      );
391      *
392      *     })();
393      *
394      * </script><pre>
395      *
396      * @example
397      * board.renderer.container.style.backgroundColor = 'lightblue';
398      * var points = [];
399      * points.push( board.create('point', [-2, 3.5], {fixed:false,color: 'yellow', size: 6,name:'6 am'}) );
400      * points.push( board.create('point', [0, 3.5],  {fixed:false,color: 'yellow', size: 6,name:'12 pm'}) );
401      * points.push( board.create('point', [2, 3.5],  {fixed:false,color: 'yellow', size: 6,name:'6 pm'}) );
402      *
403      * var fo = board.create('fo', [
404      *     '<video width="100%" height="100%" src="https://benedu.net/moodle/aaimg/ajx_img/astro/tr/1vd.mp4" type="html5video" controls>',
405      *     [-6, -4], [12, 8]],
406      *     {layer: 0, fixed: true}
407      *  );
408      *
409      * var f = JXG.Math.Numerics.lagrangePolynomial(points);
410      * var graph = board.create('functiongraph', [f, -10, 10], {fixed:true,strokeWidth:3, layer: 8});
411      *
412      * </pre><div id="JXGc3fc5520-13aa-4f66-abaa-42e9dc3fbf3f" class="jxgbox" style="width: 500px; height: 500px;"></div>
413      * <script type="text/javascript">
414      *     (function() {
415      *         var board = JXG.JSXGraph.initBoard('JXGc3fc5520-13aa-4f66-abaa-42e9dc3fbf3f',
416      *             {boundingbox: [-6,4,6,-4], axis: true, showcopyright: false, shownavigation: false});
417      *     board.renderer.container.style.backgroundColor = 'lightblue';
418      *     var points = [];
419      *     points.push( board.create('point', [-2, 3.5], {fixed:false,color: 'yellow', size: 6,name:'6 am'}) );
420      *     points.push( board.create('point', [0, 3.5],  {fixed:false,color: 'yellow', size: 6,name:'12 pm'}) );
421      *     points.push( board.create('point', [2, 3.5],  {fixed:false,color: 'yellow', size: 6,name:'6 pm'}) );
422      *
423      *     var fo = board.create('fo', [
424      *         '<video width="100%" height="100%" src="https://benedu.net/moodle/aaimg/ajx_img/astro/tr/1vd.mp4" type="html5video" controls>',
425      *         [-6, -4], [12, 8]],
426      *         {layer: 0, fixed: true}
427      *      );
428      *
429      *     var f = JXG.Math.Numerics.lagrangePolynomial(points);
430      *     var graph = board.create('functiongraph', [f, -10, 10], {fixed:true,strokeWidth:3, layer: 8});
431      *
432      *     })();
433      *
434      * </script><pre>
435      *
436      * Video "24-hour time-lapse in Cascais, Portugal. Produced by Nuno Miguel Duarte" adapted from
437      * <a href="https://www.pbslearningmedia.org/resource/buac18-k2-sci-ess-sunposition/changing-position-of-the-sun-in-the-sky/">https://www.pbslearningmedia.org/resource/buac18-k2-sci-ess-sunposition/changing-position-of-the-sun-in-the-sky/</a>,
438      * ©2016 Nuno Miguel Duarte.
439      *
440      */
441     JXG.createForeignObject = function (board, parents, attributes) {
442         var attr, fo,
443             content = parents[0],
444             coords = parents[1],
445             size = [];
446 
447         if (parents.length >= 2) {
448             size = parents[2];
449         }
450 
451         attr = Type.copyAttributes(attributes, board.options, 'foreignobject');
452         fo = CoordsElement.create(JXG.ForeignObject, board, coords, attr, content, size);
453         if (!fo) {
454             throw new Error("JSXGraph: Can't create foreignObject with parent types '" +
455                     (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
456                     "\nPossible parent types: [string, [x, y], [w, h]], [string, [x, y]], [element,transformation]");
457         }
458 
459         return fo;
460     };
461 
462     JXG.registerElement('foreignobject', JXG.createForeignObject);
463     JXG.registerElement('fo', JXG.createForeignObject);
464 
465     return {
466         ForeignObject: JXG.ForeignObject,
467         createForeignobject: JXG.createForeignObject
468     };
469 });
470