      1 /*
      2     Copyright 2008-2024
      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     this.methodMap = JXG.deepCopy(this.methodMap, {
    108         addTransformation: "addTransform",
    109         trans: "addTransform",
    110         W: "W",
    111         Width: "W",
    112         H: "H",
    113         Height: "H",
    114         setSize: "setSize"
    115     });
    116 };
    117
    118 JXG.Image.prototype = new GeometryElement();
    119 Type.copyPrototypeMethods(JXG.Image, CoordsElement, "coordsConstructor");
    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(Type.evaluate(this.visProp.precision))) {
    143                 type = this.board._inputDevice;
    144                 prec = Type.evaluate(this.visProp.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,
    228                 j,
    229                 len = this.transformations.length,
    230                 v = [];
    231
    232             if (len === 0) {
    233                 this.span = [
    234                     [this.Z(), this.X(), this.Y()],
    235                     [this.Z(), this.W(), 0],
    236                     [this.Z(), 0, this.H()]
    237                 ];
    238             } else {
    239                 // v contains the three defining corners of the rectangle/image
    240                 v[0] = [this.Z(), this.X(), this.Y()];
    241                 v[1] = [this.Z(), this.X() + this.W(), this.Y()];
    242                 v[2] = [this.Z(), this.X(), this.Y() + this.H()];
    243
    244                 // Transform the three corners
    245                 for (i = 0; i < len; i++) {
    246                     for (j = 0; j < 3; j++) {
    247                         v[j] = Mat.matVecMult(this.transformations[i].matrix, v[j]);
    248                     }
    249                 }
    250                 // Normalize the vectors
    251                 for (j = 0; j < 3; j++) {
    252                     v[j][1] /= v[j][0];
    253                     v[j][2] /= v[j][0];
    254                     v[j][0] /= v[j][0];
    255                 }
    256                 // Compute the two vectors spanning the rectangle
    257                 // by subtracting the anchor point.
    258                 for (j = 1; j < 3; j++) {
    259                     v[j][0] -= v[0][0];
    260                     v[j][1] -= v[0][1];
    261                     v[j][2] -= v[0][2];
    262                 }
    263                 this.span = v;
    264             }
    265
    266             return this;
    267         },
    268
    269         addTransform: function (transform) {
    270             var i;
    271
    272             if (Type.isArray(transform)) {
    273                 for (i = 0; i < transform.length; i++) {
    274                     this.transformations.push(transform[i]);
    275                 }
    276             } else {
    277                 this.transformations.push(transform);
    278             }
    279
    280             return this;
    281         },
    282
    283         // Documented in element.js
    284         getParents: function () {
    285             var p = [this.url, [this.Z(), this.X(), this.Y()], this.usrSize];
    286
    287             if (this.parents.length !== 0) {
    288                 p = this.parents;
    289             }
    290
    291             return p;
    292         },
    293
    294         /**
    295          * Set the width and height of the image. After setting a new size,
    296          * board.update() or image.fullUpdate()
    297          * has to be called to make the change visible.
    298          * @param  {number|function|string} width  Number, function or string
    299          *                            that determines the new width of the image
    300          * @param  {number|function|string} height Number, function or string
    301          *                            that determines the new height of the image
    302          * @returns {JXG.GeometryElement} A reference to the element
    303          *
    304          * @example
    305          * var im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg',
    306          *                                [-3,-2], [3,3]]);
    307          * im.setSize(4, 4);
    308          * board.update();
    309          *
    310          * </pre><div id="JXG8411e60c-f009-11e5-b1bf-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
    311          * <script type="text/javascript">
    312          *     (function() {
    313          *         var board = JXG.JSXGraph.initBoard('JXG8411e60c-f009-11e5-b1bf-901b0e1b8723',
    314          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    315          *     var im = board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg', [-3,-2],    [3,3]]);
    316          *     //im.setSize(4, 4);
    317          *     //board.update();
    318          *
    319          *     })();
    320          *
    321          * </script><pre>
    322          *
    323          * @example
    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          * </pre><div id="JXG4ce706c0-f00a-11e5-b1bf-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
    334          * <script type="text/javascript">
    335          *     (function() {
    336          *         var board = JXG.JSXGraph.initBoard('JXG4ce706c0-f00a-11e5-b1bf-901b0e1b8723',
    337          *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    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          *     })();
    348          *
    349          * </script><pre>
    350          *
    351          */
    352         setSize: function (width, height) {
    353             this.W = Type.createFunction(width, this.board, "");
    354             this.H = Type.createFunction(height, this.board, "");
    355             this.addParentsFromJCFunctions([this.W, this.H]);
    356             // this.fullUpdate();
    357
    358             return this;
    359         },
    360
    361         /**
    362          * Returns the width of the image in user coordinates.
    363          * @returns {number} width of the image in user coordinates
    364          */
    365         W: function () {}, // Needed for docs, defined in constructor
    366
    367         /**
    368          * Returns the height of the image in user coordinates.
    369          * @returns {number} height of the image in user coordinates
    370          */
    371         H: function () {} // Needed for docs, defined in constructor
    372     }
    373 );
    374
    375 /**
    376  * @class Displays an image.
    377  * @pseudo
    378  * @name Image
    379  * @type JXG.Image
    380  * @augments JXG.Image
    381  * @constructor
    382  * @constructor
    383  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    384  * @param {string,function_Array_Array} url,coords,size url defines the location of the image data. The array coords contains the user coordinates
    385  * of the lower left corner of the image.
    386  *   It can consist of two or three elements of type number, a string containing a GEONE<sub>x</sub>T
    387  *   constraint, or a function which takes no parameter and returns a number. Every element determines one coordinate. If a coordinate is
    388  *   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
    389  *   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
    390  *   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
    391  *   parent elements are given they will be interpreted as homogeneous coordinates.
    392  * <p>
    393  * The array size defines the image's width and height in user coordinates.
    394  * @example
    395  * var im = board.create('image', ['https://jsxgraph.org/jsxgraph/distrib/images/uccellino.jpg', [-3,-2], [3,3]]);
    396  *
    397  * </pre><div class="jxgbox" id="JXG9850cda0-7ea0-4750-981c-68bacf9cca57" style="width: 400px; height: 400px;"></div>
    398  * <script type="text/javascript">
    399  *   var image_board = JXG.JSXGraph.initBoard('JXG9850cda0-7ea0-4750-981c-68bacf9cca57', {boundingbox: [-4, 4, 4, -4], axis: true, showcopyright: false, shownavigation: false});
    400  *   var image_im = image_board.create('image', ['https://jsxgraph.org/distrib/images/uccellino.jpg', [-3,-2],[3,3]]);
    401  * </script><pre>
    402  */
    403 JXG.createImage = function (board, parents, attributes) {
    404     var attr,
    405         im,
    406         url = parents[0],
    407         coords = parents[1],
    408         size = parents[2];
    409
    410     attr = Type.copyAttributes(attributes, board.options, "image");
    411     im = CoordsElement.create(JXG.Image, board, coords, attr, url, size);
    412     if (!im) {
    413         throw new Error(
    414             "JSXGraph: Can't create image with parent types '" +
    415                 typeof parents[0] +
    416                 "' and '" +
    417                 typeof parents[1] +
    418                 "'." +
    419                 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]"
    420         );
    421     }
    422
    423     if (attr.rotate !== 0) {
    424         // This is the default value, i.e. no rotation
    425         im.addRotation(attr.rotate);
    426     }
    427
    428     return im;
    429 };
    430
    431 JXG.registerElement("image", JXG.createImage);
    432
    433 export default JXG.Image;
    434 // export default {
    435 //     Image: JXG.Image,
    436 //     createImage: JXG.createImage
    437 // };
    438