1 /*
  2     Copyright 2008-2026
  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 In this file the geometry element Image is defined.
 37  */
 38 
 39 import JXG from "../jxg.js";
 40 import Const from "./constants.js";
 41 import Coords from "./coords.js";
 42 import GeometryElement from "./element.js";
 43 import Mat from "../math/math.js";
 44 import Type from "../utils/type.js";
 45 import CoordsElement from "./coordselement.js";
 46 
 47 /**
 48  * Construct and handle images
 49  *
 50  * The image can be supplied as an URL or an base64 encoded inline image
 51  * like "data:image/png;base64, /9j/4AAQSkZJRgA..." or a function returning
 52  * an URL: function(){ return 'xxx.png; }.
 53  *
 54  * @class Creates a new image object. Do not use this constructor to create a image. Use {@link JXG.Board#create} with
 55  * type {@link Image} instead.
 56  * @augments JXG.GeometryElement
 57  * @augments JXG.CoordsElement
 58  * @param {string|JXG.Board} board The board the new image is drawn on.
 59  * @param {Array} coordinates An array with the user coordinates of the image.
 60  * @param {Object} attributes An object containing visual and - optionally - a name and an id.
 61  * @param {string|function} url An URL string or a function returning an URL string.
 62  * @param  {Array} size Array containing width and height of the image in user coordinates.
 63  *
 64  */
 65 JXG.Image = function (board, coords, attributes, url, size) {
 66     this.constructor(board, attributes, Const.OBJECT_TYPE_IMAGE, Const.OBJECT_CLASS_OTHER);
 67     this.element = this.board.select(attributes.anchor);
 68     this.coordsConstructor(coords);
 69 
 70     this.W = Type.createFunction(size[0], this.board, "");
 71     this.H = Type.createFunction(size[1], this.board, "");
 72     this.addParentsFromJCFunctions([this.W, this.H]);
 73 
 74     this.usrSize = [this.W(), this.H()];
 75 
 76     /**
 77      * Array of length two containing [width, height] of the image in pixel.
 78      * @type array
 79      */
 80     this.size = [
 81         Math.abs(this.usrSize[0] * board.unitX),
 82         Math.abs(this.usrSize[1] * board.unitY)
 83     ];
 84 
 85     /**
 86      * 'href' of the image. This might be an URL, but also a data-uri is allowed.
 87      * @type string
 88      */
 89     this.url = url;
 90 
 91     this.elType = 'image';
 92 
 93     // span contains the anchor point and the two vectors
 94     // spanning the image rectangle.
 95     this.span = [
 96         this.coords.usrCoords.slice(0),
 97         [this.coords.usrCoords[0], this.W(), 0],
 98         [this.coords.usrCoords[0], 0, this.H()]
 99     ];
100 
101     //this.parent = board.select(attributes.anchor);
102     this.id = this.board.setId(this, 'Im');
103 
104     this.board.renderer.drawImage(this);
105     this.board.finalizeAdding(this);
106 };
107 
108 JXG.Image.prototype = new GeometryElement();
109 
110 Type.copyPrototypeMethods(JXG.Image, CoordsElement, 'coordsConstructor');
111 Type.copyMethodMap(JXG.Image, {
112     addTransformation: "addTransform",
113     trans: "addTransform",
114     W: "W",
115     Width: "W",
116     H: "H",
117     Height: "H",
118     setSize: "setSize"
119 });
120 
121 JXG.extend(
122     JXG.Image.prototype,
123     /** @lends JXG.Image.prototype */ {
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,
132                 dy,
133                 r,
134                 type,
135                 prec,
136                 c,
137                 v,
138                 p,
139                 dot,
140                 len = this.transformations.length;
141 
142             if (Type.isObject(this.evalVisProp('precision'))) {
143                 type = this.board._inputDevice;
144                 prec = this.evalVisProp('precision.' + type);
145             } else {
146                 // 'inherit'
147                 prec = this.board.options.precision.hasPoint;
148             }
149 
150             // Easy case: no transformation
151             if (len === 0) {
152                 dx = x - this.coords.scrCoords[1];
153                 dy = this.coords.scrCoords[2] - y;
154                 r = prec;
155 
156                 return dx >= -r && dx - this.size[0] <= r && dy >= -r && dy - this.size[1] <= r;
157             }
158 
159             // Image is transformed
160             c = new Coords(Const.COORDS_BY_SCREEN, [x, y], this.board);
161             // v is the vector from anchor point to the drag point
162             c = c.usrCoords;
163             v = [c[0] - this.span[0][0], c[1] - this.span[0][1], c[2] - this.span[0][2]];
164             dot = Mat.innerProduct; // shortcut
165 
166             // Project the drag point to the sides.
167             p = dot(v, this.span[1]);
168             if (0 <= p && p <= dot(this.span[1], this.span[1])) {
169                 p = dot(v, this.span[2]);
170 
171                 if (0 <= p && p <= dot(this.span[2], this.span[2])) {
172                     return true;
173                 }
174             }
175             return false;
176         },
177 
178         /**
179          * Recalculate the coordinates of lower left corner and the width and height.
180          *
181          * @returns {JXG.GeometryElement} A reference to the element
182          * @private
183          */
184         update: function (fromParent) {
185             if (!this.needsUpdate) {
186                 return this;
187             }
188 
189             this.updateCoords(fromParent);
190             this.updateSize();
191             this.updateSpan();
192 
193             return this;
194         },
195 
196         /**
197          * Send an update request to the renderer.
198          * @private
199          */
200         updateRenderer: function () {
201             return this.updateRendererGeneric('updateImage');
202         },
203 
204         /**
205          * Updates the internal arrays containing size of the image.
206          * @returns {JXG.GeometryElement} A reference to the element
207          * @private
208          */
209         updateSize: function () {
210             this.usrSize = [this.W(), this.H()];
211             this.size = [
212                 Math.abs(this.usrSize[0] * this.board.unitX),
213                 Math.abs(this.usrSize[1] * this.board.unitY)
214             ];
215 
216             return this;
217         },
218 
219         /**
220          * Update the anchor point of the image, i.e. the lower left corner
221          * and the two vectors which span the rectangle.
222          * @returns {JXG.GeometryElement} A reference to the element
223          * @private
224          *
225          */
226         updateSpan: function () {
227             var i, j,
228                 len = this.transformations.length,
229                 v = [];
230 
231             if (len === 0) {
232                 this.span = [
233                     [this.Z(), this.X(), this.Y()],
234                     [this.Z(), this.W(), 0],
235                     [this.Z(), 0, this.H()]
236                 ];
237             } else {
238                 // v contains the three defining corners of the rectangle/image
239                 v[0] = [this.Z(), this.X(), this.Y()];
240                 v[1] = [this.Z(), this.X() + this.W(), this.Y()];
241                 v[2] = [this.Z(), this.X(), this.Y() + this.H()];
242                 // Transform the three corners
243                 for (i = 0; i < len; i++) {
244                     for (j = 0; j < 3; j++) {
245                         v[j] = Mat.matVecMult(this.transformations[i].matrix, v[j]);
246                     }
247                 }
248                 // Normalize the vectors
249                 for (j = 0; j < 3; j++) {
250                     v[j][1] /= v[j][0];
251                     v[j][2] /= v[j][0];
252                     v[j][0] /= v[j][0];
253                 }
254                 // Compute the two vectors spanning the rectangle
255                 // by subtracting the anchor point.
256                 for (j = 1; j < 3; j++) {
257                     v[j][0] -= v[0][0];
258                     v[j][1] -= v[0][1];
259                     v[j][2] -= v[0][2];
260                 }
261                 this.span = v;
262             }
263 
264             return this;
265         },
266 
267         addTransform: function (transform) {
268             var i,
269                 list = Type.isArray(transform) ? transform : [transform],
270                 len = list.length;
271 
272             for (i = 0; i < len; i++) {
273                 this.transformations.push(list[i]);
274             }
275 
276             return this;
277         },
278 
279         removeTransform: function (transform) {
280             var i,
281                 list = Type.isArray(transform) ? transform : [transform],
282                 len = list.length;
283 
284             for (i = 0; i < len; i++) {
285                 Type.removeElementFromArray(this.transformations, list[i]);
286             }
287 
288             return this;
289         },
290 
291         clearTransforms: function () {
292             this.transformations = [];
293 
294             return this;
295         },
296 
297         // Documented in element.js
298         getParents: function () {
299             var p = [this.url, [this.Z(), this.X(), this.Y()], this.usrSize];
300 
301             if (this.parents.length !== 0) {
302                 p = this.parents;
303             }
304 
305             return p;
306         },
307 
308         /**
309          * Set the width and height of the image. After setting a new size,
310          * board.update() or image.fullUpdate()
311          * has to be called to make the change visible.
312          * @param  {number|function|string} width  Number, function or string
313          *                            that determines the new width of the image
314          * @param  {number|function|string} height Number, function or string
315          *                            that determines the new height of the image
316          * @returns {JXG.GeometryElement} A reference to the element
317          *
318          * @example
319          * var im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg',
320          *                                [-3,-2], [3,3]]);
321          * im.setSize(4, 4);
322          * board.update();
323          *
324          * </pre><div id="JXG8411e60c-f009-11e5-b1bf-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
325          * <script type="text/javascript">
326          *     (function() {
327          *         var board = JXG.JSXGraph.initBoard('JXG8411e60c-f009-11e5-b1bf-901b0e1b8723',
328          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
329          *     var im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg', [-3,-2],    [3,3]]);
330          *     //im.setSize(4, 4);
331          *     //board.update();
332          *
333          *     })();
334          *
335          * </script><pre>
336          *
337          * @example
338          * var p0 = board.create('point', [-3, -2]),
339          *     im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg',
340          *                     [function(){ return p0.X(); }, function(){ return p0.Y(); }],
341          *                     [3,3]]),
342          *     p1 = board.create('point', [1, 2]);
343          *
344          * im.setSize(function(){ return p1.X() - p0.X(); }, function(){ return p1.Y() - p0.Y(); });
345          * board.update();
346          *
347          * </pre><div id="JXG4ce706c0-f00a-11e5-b1bf-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
348          * <script type="text/javascript">
349          *     (function() {
350          *         var board = JXG.JSXGraph.initBoard('JXG4ce706c0-f00a-11e5-b1bf-901b0e1b8723',
351          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
352          *     var p0 = board.create('point', [-3, -2]),
353          *         im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg',
354          *                         [function(){ return p0.X(); }, function(){ return p0.Y(); }],
355          *                         [3,3]]),
356          *         p1 = board.create('point', [1, 2]);
357          *
358          *     im.setSize(function(){ return p1.X() - p0.X(); }, function(){ return p1.Y() - p0.Y(); });
359          *     board.update();
360          *
361          *     })();
362          *
363          * </script><pre>
364          *
365          */
366         setSize: function (width, height) {
367             this.W = Type.createFunction(width, this.board, "");
368             this.H = Type.createFunction(height, this.board, "");
369             this.addParentsFromJCFunctions([this.W, this.H]);
370             // this.fullUpdate();
371 
372             return this;
373         },
374 
375         /**
376          * Returns the width of the image in user coordinates.
377          * @returns {number} width of the image in user coordinates
378          */
379         W: function () {}, // Needed for docs, defined in constructor
380 
381         /**
382          * Returns the height of the image in user coordinates.
383          * @returns {number} height of the image in user coordinates
384          */
385         H: function () {} // Needed for docs, defined in constructor
386     }
387 );
388 
389 /**
390  * @class Display of an external image.
391  * @pseudo
392  * @name Image
393  * @type JXG.Image
394  * @augments JXG.Image
395  * @constructor
396  * @constructor
397  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
398  * @param {string,function_Array_Array} url,coords,size url defines the location of the image data. The array coords contains the user coordinates
399  * of the lower left corner of the image.
400  *   It can consist of two or three elements of type number, a string containing a GEONE<sub>x</sub>T
401  *   constraint, or a function which takes no parameter and returns a number. Every element determines one coordinate. If a coordinate is
402  *   given by a number, the number determines the initial position of a free image. If given by a string or a function that coordinate will be constrained
403  *   that means the user won't be able to change the image's position directly by mouse because it will be calculated automatically depending on the string
404  *   or the function's return value. If two parent elements are given the coordinates will be interpreted as 2D affine Euclidean coordinates, if three such
405  *   parent elements are given they will be interpreted as homogeneous coordinates.
406  * <p>
407  * The array size defines the image's width and height in user coordinates.
408  * @example
409  * var im = board.create('image', ['https://jsxgraph.org/jsxgraph/distrib/images/uccellino.jpg', [-3,-2], [3,3]]);
410  *
411  * </pre><div class="jxgbox" id="JXG9850cda0-7ea0-4750-981c-68bacf9cca57" style="width: 400px; height: 400px;"></div>
412  * <script type="text/javascript">
413  *   var image_board = JXG.JSXGraph.initBoard('JXG9850cda0-7ea0-4750-981c-68bacf9cca57', {boundingbox: [-4, 4, 4, -4], axis: true, showcopyright: false, shownavigation: false});
414  *   var image_im = image_board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg', [-3,-2],[3,3]]);
415  * </script><pre>
416  */
417 JXG.createImage = function (board, parents, attributes) {
418     var attr,
419         im,
420         url = parents[0],
421         coords = parents[1],
422         size = parents[2];
423 
424     attr = Type.copyAttributes(attributes, board.options, 'image');
425     im = CoordsElement.create(JXG.Image, board, coords, attr, url, size);
426     if (!im) {
427         throw new Error(
428             "JSXGraph: Can't create image with parent types '" +
429                 typeof parents[0] +
430                 "' and '" +
431                 typeof parents[1] +
432                 "'." +
433                 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]"
434         );
435     }
436 
437     if (attr.rotate !== 0) {
438         // This is the default value, i.e. no rotation
439         im.addRotation(attr.rotate);
440     }
441 
442     return im;
443 };
444 
445 JXG.registerElement("image", JXG.createImage);
446 
447 export default JXG.Image;
448 // export default {
449 //     Image: JXG.Image,
450 //     createImage: JXG.createImage
451 // };
452