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