activities_panels_Identify.js

/**
 *  File    : activities/panels/Identify.js
 *  Created : 03/06/2015
 *  By      : Francesc Busquets <francesc@gmail.com>
 *
 *  JClic.js
 *  An HTML5 player of JClic activities
 *  https://projectestac.github.io/jclic.js
 *
 *  @source https://github.com/projectestac/jclic.js
 *
 *  @license EUPL-1.2
 *  @licstart
 *  (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
 *
 *  Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
 *  the European Commission- subsequent versions of the EUPL (the "Licence");
 *  You may not use this work except in compliance with the Licence.
 *
 *  You may obtain a copy of the Licence at:
 *  https://joinup.ec.europa.eu/software/page/eupl
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  Licence for the specific language governing permissions and limitations
 *  under the Licence.
 *  @licend
 *  @module
 */

/* global window */

import $ from 'jquery';
import { Activity, ActivityPanel } from '../../Activity.js';
import ActiveBoxGrid from '../../boxes/ActiveBoxGrid.js';
import BoxBag from '../../boxes/BoxBag.js';
import { Rectangle, Point } from '../../AWT.js';

/**
 * The aim of this type of {@link module:Activity.Activity Activity} is to identify {@link module:boxes/ActiveBox.ActiveBox ActiveBox} elements in a panel
 * that satisfy a specific condition, usually exposed in the main message.
 * @extends module:Activity.Activity
 */
export class Identify extends Activity {
  /**
   * Identify constructor
   * @param {module:project/JClicProject.JClicProject} project - The {@link module:project/JClicProject.JClicProject JClicProject} to which this activity belongs
   */
  constructor(project) {
    super(project);
  }

  /**
   * Retrieves the minimum number of actions needed to solve this activity
   * @override
   * @returns {number}
   */
  getMinNumActions() {
    return this.cellsToMatch;
  }

  /**
   * Whether or not the activity uses random to shuffle internal components
   * @override
   * @returns {boolean}
   */
  hasRandom() {
    return true;
  }
}

Object.assign(Identify.prototype, {
  /**
   * Number of not assigned cells (calculated in {@link module:activities/panels/Identify.IdentifyPanel#buildVisualComponents buildVisualComponents})
   * @name module:activities/panels/Identify.Identify#nonAssignedCells
   * @type {number} */
  nonAssignedCells: 0,
  /**
   * Number of cells the user must identify to complete the activity (calculated in
   * {@link module:activities/panels/Identify.IdentifyPanel#buildVisualComponents buildVisualComponents})
   * @name module:activities/panels/Identify.Identify#cellsToMatch
   * @type {number} */
  cellsToMatch: 1,
});

/**
 * The {@link module:Activity.ActivityPanel ActivityPanel} where {@link module:activities/panels/Identify.Identify Identify} activities are played.
 * @extends module:Activity.ActivityPanel
 */
export class IdentifyPanel extends ActivityPanel {
  /**
   * IdentifyPanel constructor
   * @param {module:Activity.Activity} act - The {@link module:Activity.Activity Activity} to which this Panel belongs
   * @param {module:JClicPlayer.JClicPlayer} ps - Any object implementing the methods defined in the
   * [PlayStation](http://projectestac.github.io/jclic/apidoc/edu/xtec/jclic/PlayStation.html) Java interface.
   * @param {external:jQuery} [$div] - The jQuery DOM element where this Panel will deploy
   */
  constructor(act, ps, $div) {
    super(act, ps, $div);
  }

  /**
   * Miscellaneous cleaning operations
   * @override
   */
  clear() {
    if (this.bg) {
      this.bg.end();
      this.bg = null;
    }
  }

  /**
   * Prepares the visual components of the activity
   * @override
   */
  buildVisualComponents() {
    if (this.firstRun)
      super.buildVisualComponents();
    this.clear();
    const
      abc = this.act.abc['primary'],
      solved = this.act.abc['solvedPrimary'];
    if (abc) {
      if (abc.image) {
        abc.setImgContent(this.act.project.mediaBag, null, false);
        if (abc.animatedGifFile && !abc.shaper.rectangularShapes && !this.act.shuffleA)
          this.$animatedBg = $('<span/>').css({
            'background-image': `url(${abc.animatedGifFile})`,
            'background-position': 'center',
            'background-repeat': 'no-repeat',
            position: 'absolute'
          }).appendTo(this.$div);
      }

      if (solved && solved.image)
        solved.setImgContent(this.act.project.mediaBag, null, false);

      if (this.act.acp !== null) {
        const contentKit = [abc];
        if (solved) {
          contentKit.push(null);
          contentKit.push(solved);
        }
        this.act.acp.generateContent(abc.nch, abc.ncw, contentKit, false);
      }
      this.bg = ActiveBoxGrid.createEmptyGrid(null, this,
        this.act.margin, this.act.margin,
        abc);
      this.bg.setContent(abc, solved || null);
      this.bg.setAlternative(false);
      if (this.$animatedBg)
        this.bg.setCellAttr('tmpTrans', true);
      this.bg.setDefaultIdAss();
      this.act.nonAssignedCells = 0;
      this.act.cellsToMatch = 0;
      const n = this.bg.getNumCells();
      for (let i = 0; i < n; i++) {
        const
          bx = this.bg.getActiveBox(i),
          id = bx.idAss;
        if (id === 1)
          this.act.cellsToMatch++;
        else if (id === -1) {
          this.act.nonAssignedCells++;
          bx.switchToAlt(this.ps);
        }
      }
      this.bg.setVisible(true);
    }
  }

  /**
   * Basic initialization procedure
   * @override
   */
  initActivity() {
    super.initActivity();
    if (!this.firstRun)
      this.buildVisualComponents();
    else
      this.firstRun = false;

    if (this.bg) {
      if (this.act.shuffleA)
        this.shuffle([this.bg], true, true);

      if (this.useOrder)
        this.currentItem = this.bg.getNextItem(-1);

      this.setAndPlayMsg('initial', 'start');
      this.invalidate().update();
      this.playing = true;
    }
  }

  /**
   * Updates the graphic content of this panel.
   * This method will be called from {@link module:AWT.Container#update} when needed.
   * @override
   * @param {module:AWT.Rectangle} dirtyRegion - Specifies the area to be updated. When `null`,
   * it's the whole panel.
   */
  updateContent(dirtyRegion) {
    super.updateContent(dirtyRegion);

    if (this.bg && this.$canvas) {
      const
        canvas = this.$canvas.get(-1),
        ctx = canvas.getContext('2d');
      if (!dirtyRegion)
        dirtyRegion = new Rectangle(0, 0, canvas.width, canvas.height);
      ctx.clearRect(dirtyRegion.pos.x, dirtyRegion.pos.y, dirtyRegion.dim.width, dirtyRegion.dim.height);
      this.bg.update(ctx, dirtyRegion);
    }
    return super.updateContent(dirtyRegion);
  }

  /**
   * Sets the real dimension of this panel.
   * @override
   * @param {module:AWT.Dimension} preferredMaxSize - The maximum surface available for the activity panel
   * @returns {module:AWT.Dimension}
   */
  setDimension(preferredMaxSize) {
    return this.getBounds().equals(preferredMaxSize) ?
      preferredMaxSize :
      BoxBag.layoutSingle(preferredMaxSize, this.bg, this.act.margin);
  }

  /**
   * Sets the size and position of this activity panel
   * @override
   * @param {module:AWT.Rectangle} rect
   */
  setBounds(rect) {
    if (this.$canvas)
      this.$canvas.remove();

    super.setBounds(rect);
    if (this.bg) {
      this.$canvas = $(`<canvas width="${rect.dim.width}" height="${rect.dim.height}"/>`).css({
        position: 'absolute',
        top: 0,
        left: 0
      });
      // Resize animated gif background
      if (this.$animatedBg) {
        const bgRect = this.bg.getBounds();
        this.$animatedBg.css({
          left: bgRect.pos.x,
          top: bgRect.pos.y,
          width: `${bgRect.dim.width}px`,
          height: `${bgRect.dim.height}px`,
          'background-size': `${bgRect.dim.width}px ${bgRect.dim.height}px`
        });
      }
      this.$div.append(this.$canvas);
      this.invalidate().update();
      window.setTimeout(() => this.bg ? this.bg.buildAccessibleElements(this.$canvas, this.$div) : null, 0);
    }
  }

  /**
   * Builds the accessible components needed for this ActivityPanel
   * This method is called when all main elements are placed and visible, when the activity is ready
   * to start or when resized.
   * @override
   */
  buildAccessibleComponents() {
    if (this.bg && this.$canvas && this.accessibleCanvas) {
      super.buildAccessibleComponents();
      this.bg.buildAccessibleElements(this.$canvas, this.$div);
    }
  }

  /**
   * Main handler used to process mouse, touch, keyboard and edit events
   * @override
   * @param {external:Event} event - The HTML event to be processed
   * @returns {boolean} - When this event handler returns `false`, jQuery will stop its
   * propagation through the DOM tree. See: {@link http://api.jquery.com/on}
   */
  processEvent(event) {
    if (this.playing) {
      const p = new Point(
        event.pageX - this.$div.offset().left,
        event.pageY - this.$div.offset().top);
      // Flag for assuring that only one media plays per event (avoid event sounds overlapping
      // cell's media sounds)
      let m = false;
      // Array to be filled with actions to be executed at the end of event processing
      const delayedActions = [];

      switch (event.type) {
        case 'click':
          this.ps.stopMedia(1);
          // Find the box behind the clicked point
          const bx = this.bg ? this.bg.findActiveBox(p) : null;
          if (bx) {
            if (bx.idAss !== -1) {
              // Check if it's a valid move
              let ok = false;
              const src = bx.getDescription();
              m = m || bx.playMedia(this.ps, delayedActions);
              if (bx.idAss === 1 && (!this.act.useOrder || bx.idOrder === this.currentItem)) {
                ok = true;
                bx.idAss = -1;
                if (bx.switchToAlt(this.ps))
                  m = m || bx.playMedia(this.ps, delayedActions);
                else
                  bx.clear();
                if (this.act.useOrder)
                  this.currentItem = this.bg.getNextItem(this.currentItem, 1);
              }
              const cellsOk = this.bg.countCellsWithIdAss(-1);
              this.ps.reportNewAction(this.act, 'SELECT', src, null, ok, cellsOk - this.act.nonAssignedCells);
              if (ok && cellsOk === this.act.cellsToMatch + this.act.nonAssignedCells)
                this.finishActivity(true);
              else if (!m)
                this.playEvent(ok ? 'actionOk' : 'actionError');
              this.update();
            } else {
              this.playEvent('actionError');
            }
          }
          break;
      }
      delayedActions.forEach(action => action());
      event.preventDefault();
    }
  }
}

Object.assign(IdentifyPanel.prototype, {
  /**
   * The {@link module:boxes/ActiveBoxbag.ActiveBoxBag ActiveBoxBag} containing the information to be displayed on the panel.
   * @name module:activities/panels/Identify.IdentifyPanel#bg
   * @type {module:boxes/ActiveBoxBag.ActiveBoxBag} */
  bg: null,
  /**
   * List of mouse, touch and keyboard events intercepted by this panel
   * @override
   * @name module:activities/panels/Identify.IdentifyPanel#events
   * @type {string[]} */
  events: ['click'],
});

/**
 * Panel class associated to this type of activity: {@link module:activities/panels/Identify.IdentifyPanel IdentifyPanel}
 * @type {class} */
Identify.Panel = IdentifyPanel;

// Register activity class
export default Activity.registerClass('@panels.Identify', Identify);