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