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