<?xml version="1.0" encoding="UTF-8"?>
<quiz>
  <question type="stack">
    <name>
      <text>4. JSON and complex binding.</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p>The previous examples have shown the use of simplified binding of single primitives to single inputs, that should cover most use cases but once you start to do more complex things you will need to start building your own binding logic. Some examples of such situations are:</p><ul><li>Creation and deletion of arbitrary numbers of points/objects in graph cannot be tracked with one input per object logic as you cannot create inputs. In these cases you need to store more structure into inputs.</li><li>Not dealing with the positions of things might also be an obvious case, if you want to for example track the order the student clicks some edges in some graph use of the simple binding tools will not help.</li></ul><p>Basically, you cannot use one-to-one input-object-binding if you have dynamic number of objects. To deal with this you need to have some form of data-structures (simple lists of coordinates are often enough but once you have multiple types of things with connections between them you will probably want more) that you can store that represent the state of your graph. As you are authoring JSXGraph graphs in JavaScript the obvious structures are JavaScript objects/dictionaries and lists, luckily those can easily be turned to JSON strings that can be stored into String input fields. And what is important STACK now provides a way for parsing those JSON strings on Maxima side so that you can process the values there.</p><pre style="line-height:0.9;">[[escape value="[[jsxgraph height='700px' width='700px' input-ref-ans1='ans1Ref']]"/]]
/* In this example ans1 is a String type input, which is important as the value we store 
   there is most definitely not a valid Maxima expression. */
var board = JXG.JSXGraph.initBoard(divid, {boundingbox: [-5, 5, 5, -5],showNavigation:false});

/* We will store our points here. */
var points = new Array();
var i = 0;

/* If we have stored state of points we will resurrect them now. */
var theInput = document.getElementById(ans1Ref);
if (theInput.value && theInput.value != '') {
 var pointList = JSON.parse(theInput.value);
 for (i=0; i&lt;pointList.length; i++) {
  var p = board.create('point', [pointList[i]['x'],pointList[i]['y']], {name:'',color:pointList[i]['color']});
  points[i] = p;
 } 
}

/* Lets create some magic, in this example dragging points through specific areas changes 
   the color of the points. And there is an area that both creates and deletes points. */

var source = board.create('circle',[board.create('point',[-4,-4],{visible: false}),0.7], {  strokeColor:'black',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var red = board.create('circle',[board.create('point',[0,-4],{visible: false}),0.7], {  strokeColor:'red',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var green = board.create('circle',[board.create('point',[2,-4],{visible: false}),0.7], {    strokeColor:'green',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var blue = board.create('circle',[board.create('point',[4,-4],{visible: false}),0.7], { strokeColor:'blue',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});

/* A function the we can tie to points so that their color changes. */
var colorDragListener = function() {
 /* this in this context should be the point. */
 if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) &lt; source.Radius()) {
  this.setAttribute({color:'black'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[red.center.X(),red.center.Y()]) &lt; red.Radius()) {
  this.setAttribute({color:'red'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[green.center.X(),green.center.Y()]) &lt; green.Radius()) {
  this.setAttribute({color:'green'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[blue.center.X(),blue.center.Y()]) &lt; blue.Radius()) {
  this.setAttribute({color:'blue'});
 }
}

/* Another function to handle the release of dragged points and the storage of the state. */
var dropHandler = function() {
 /* this in this context should be the point. */
 var newOne = points.indexOf(this);
 if (newOne == -1) {
  /* This is a new one depending on where it was dropped we do things. */
  if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) &lt; source.Radius()) {
   /* When dropped into the source, reset the point. */
   this.moveTo([source.center.X(),source.center.Y()],100);
   this.setAttribute({color:'black'});
  } else {
   /* it is now in play add it to the list. */
   points[points.length] = this;
   /* also initialise a new point in the source. */
   var p = board.create('point', [source.center.X(),source.center.Y()], {name:'',color:'black'});
   p.on('drag',colorDragListener);
   p.on('up',dropHandler);
  }
 } else {
  if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) &lt; source.Radius()) {
   /* When dropped into the source delete this point as this is an existing point. */
   points.splice(newOne, 1);
   this.board.removeObject(this);
  }
  /* Otherwise all fine. */
 }

 /* Then construct the state. Basically, map the points to a specific form, in this example the form is 
    a bit too complex as it includes an object/dictionary when a list would have worked but maybe this
    will come in handy elsewhere. */
 var newState = new Array();
 for (i=0; i&lt;points.length; i++) {
  var P = {'x':points[i].X(),'y':points[i].Y(),'color':points[i].getAttribute('color')};
  newState[i] = P;
 }
 theInput.value = JSON.stringify(newState);

 /* NOTE! You will need to do certain things to make the VLE around you know that things happen, 
    for this reason we need to generate a "change" event. In the case of Moodle and this question the event
    marks the form modified and causes a warning if the page is left through non saving routes. */
 var e = new Event('change');
 theInput.dispatchEvent(e);
}


/* This is the intial point. */
var p = board.create('point', [source.center.X(),source.center.Y()], {name:'',color:'black'});
p.on('drag',colorDragListener);
p.on('up',dropHandler);

/* Remember that the points from stored state must also be tied to the listeners if present. */
for (i=0; i&lt;points.length; i++) {
 points[i].on('drag',colorDragListener);
 points[i].on('up',dropHandler);
}

/* Always call board.update() at the end if you have bindings. */
board.update();

/* In this example we hide the input as it will get absurd very soon. */
document.getElementById(ans1Ref).style.display = 'none';
[[escape value="[[/jsxgraph]]"/]]
</pre>

[[jsxgraph height='700px' width='700px' input-ref-ans1='ans1Ref']]
/* In this example ans1 is a String type input, which is important as the value we store 
   there is most definitely not a valid Maxima expression. */
var board = JXG.JSXGraph.initBoard(divid, {boundingbox: [-5, 5, 5, -5],showNavigation:false});

/* We will store our points here. */
var points = new Array();
var i = 0;

/* If we have stored state of points we will resurrect them now. */
var theInput = document.getElementById(ans1Ref);
if (theInput.value && theInput.value != '') {
 var pointList = JSON.parse(theInput.value);
 for (i=0; i<pointList.length; i++) {
  var p = board.create('point', [pointList[i]['x'],pointList[i]['y']], {name:'',color:pointList[i]['color']});
  points[i] = p;
 } 
}

/* Lets create some magic, in this example dragging points through specific areas changes 
   the color of the points. And there is an area that both creates and deletes points. */

var source = board.create('circle',[board.create('point',[-4,-4],{visible: false}),0.7], {  strokeColor:'black',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var red = board.create('circle',[board.create('point',[0,-4],{visible: false}),0.7], {  strokeColor:'red',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var green = board.create('circle',[board.create('point',[2,-4],{visible: false}),0.7], {    strokeColor:'green',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});
var blue = board.create('circle',[board.create('point',[4,-4],{visible: false}),0.7], { strokeColor:'blue',frozen:true, fixed:true, method:'pointRadius',hasInnerPoints:true});

/* A function the we can tie to points so that their color changes. */
var colorDragListener = function() {
 /* this in this context should be the point. */
 if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) < source.Radius()) {
  this.setAttribute({color:'black'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[red.center.X(),red.center.Y()]) < red.Radius()) {
  this.setAttribute({color:'red'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[green.center.X(),green.center.Y()]) < green.Radius()) {
  this.setAttribute({color:'green'});
 } else if (JXG.Math.Geometry.distance([this.X(),this.Y()],[blue.center.X(),blue.center.Y()]) < blue.Radius()) {
  this.setAttribute({color:'blue'});
 }
}

/* Another function to handle the release of dragged points and the storage of the state. */
var dropHandler = function() {
 /* this in this context should be the point. */
 var newOne = points.indexOf(this);
 if (newOne == -1) {
  /* This is a new one depending on where it was dropped we do things. */
  if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) < source.Radius()) {
   /* When dropped into the source, reset the point. */
   this.moveTo([source.center.X(),source.center.Y()],100);
   this.setAttribute({color:'black'});
  } else {
   /* it is now in play add it to the list. */
   points[points.length] = this;
   /* also initialise a new point in the source. */
   var p = board.create('point', [source.center.X(),source.center.Y()], {name:'',color:'black'});
   p.on('drag',colorDragListener);
   p.on('up',dropHandler);
  }
 } else {
  if (JXG.Math.Geometry.distance([this.X(),this.Y()],[source.center.X(),source.center.Y()]) < source.Radius()) {
   /* When dropped into the source delete this point as this is an existing point. */
   points.splice(newOne, 1);
   this.board.removeObject(this);
  }
  /* Otherwise all fine. */
 }

 /* Then construct the state. Basically, map the points to a specific form, in this example the form is 
    a bit too complex as it includes an object/dictionary when a list would have worked but maybe this
    will come in handy elsewhere. */
 var newState = new Array();
 for (i=0; i<points.length; i++) {
  var P = {'x':points[i].X(),'y':points[i].Y(),'color':points[i].getAttribute('color')};
  newState[i] = P;
 }
 theInput.value = JSON.stringify(newState);

 /* NOTE! You will need to do certain things to make the VLE around you know that things happen, 
    for this reason we need to generate a "change" event. In the case of Moodle and this question the event
    marks the form modified and causes a warning if the page is left through non saving routes. */
 var e = new Event('change');
 theInput.dispatchEvent(e);
}


/* This is the intial point. */
var p = board.create('point', [source.center.X(),source.center.Y()], {name:'',color:'black'});
p.on('drag',colorDragListener);
p.on('up',dropHandler);

/* Remember that the points from stored state must also be tied to the listeners if present. */
for (i=0; i<points.length; i++) {
 points[i].on('drag',colorDragListener);
 points[i].on('up',dropHandler);
}

/* Always call board.update() at the end if you have bindings. */
board.update();

/* In this example we hide the input as it will get absurd very soon. */
document.getElementById(ans1Ref).style.display = 'none';
[[/jsxgraph]]

<p>Drag some points around and press check, as expected the points stay in place. The feedback should also show you the raw stored state and some very simple numbers collected from it. You may wish to check the feedback logic but it has very little to show.</p><p>In STACK 4.4 a simplified way of doing custom binding was added, if you need to do this you might wish to look into it, there should be a new sample of that.</p><p>[[input:ans1]] [[validation:ans1]]</p>]]></text>
    </questiontext>
    <generalfeedback format="moodle_auto_format">
      <text/>
    </generalfeedback>
    <defaultgrade>1</defaultgrade>
    <penalty>0.1</penalty>
    <hidden>0</hidden>
    <idnumber/>
    <stackversion>
      <text>2020040300</text>
    </stackversion>
    <questionvariables>
      <text/>
    </questionvariables>
    <specificfeedback format="html">
      <text>[[feedback:prt1]]</text>
    </specificfeedback>
    <questionnote format="html">
      <text/>
    </questionnote>
    <questiondescription format="html">
      <text/>
    </questiondescription>
    <questionsimplify>1</questionsimplify>
    <assumepositive>0</assumepositive>
    <assumereal>0</assumereal>
    <prtcorrect format="html">
      <text><![CDATA[<span style="font-size: 1.5em; color:green;"><i class="fa fa-check"></i></span> Correct answer, well done.]]></text>
    </prtcorrect>
    <prtpartiallycorrect format="html">
      <text><![CDATA[<span style="font-size: 1.5em; color:orange;"><i class="fa fa-adjust"></i></span> Your answer is partially correct.]]></text>
    </prtpartiallycorrect>
    <prtincorrect format="html">
      <text><![CDATA[<span style="font-size: 1.5em; color:red;"><i class="fa fa-times"></i></span> Incorrect answer.]]></text>
    </prtincorrect>
    <decimals>.</decimals>
    <scientificnotation>*10</scientificnotation>
    <multiplicationsign>dot</multiplicationsign>
    <sqrtsign>1</sqrtsign>
    <complexno>i</complexno>
    <inversetrig>cos-1</inversetrig>
    <logicsymbol>lang</logicsymbol>
    <matrixparens>[</matrixparens>
    <isbroken>0</isbroken>
    <variantsselectionseed/>
    <input>
      <name>ans1</name>
      <type>string</type>
      <tans><![CDATA[""]]></tans>
      <boxsize>15</boxsize>
      <strictsyntax>1</strictsyntax>
      <insertstars>0</insertstars>
      <syntaxhint/>
      <syntaxattribute>0</syntaxattribute>
      <forbidwords/>
      <allowwords/>
      <forbidfloat>0</forbidfloat>
      <requirelowestterms>0</requirelowestterms>
      <checkanswertype>0</checkanswertype>
      <mustverify>0</mustverify>
      <showvalidation>0</showvalidation>
      <options/>
    </input>
    <prt>
      <name>prt1</name>
      <value>1.0000000</value>
      <autosimplify>1</autosimplify>
      <feedbackstyle>1</feedbackstyle>
      <feedbackvariables>
        <text><![CDATA[/* First parse the input string, depending on what it is we can get a list, a stackmap, an integer, a float, a string or und if nothing is found. */
parsed: stackjson_parse(ans1);
/* As we will most likely get an list of stackmaps we can just split that list based on which of the elements have which colors like this. We may get und, do try to figure out why and when. */
red:if is(parsed=und) then 0 else length(sublist(parsed, lambda([x], stackmap_get(x,"color")="red")));
green:if is(parsed=und) then 0 else length(sublist(parsed, lambda([x], stackmap_get(x,"color")="green")));
blue:if is(parsed=und) then 0 else length(sublist(parsed, lambda([x], stackmap_get(x,"color")="blue")));
black:if is(parsed=und) then 0 else length(sublist(parsed, lambda([x], stackmap_get(x,"color")="black")));
/* As an exercise you might want to evaluate the means of coordinates of each color group and draw a suitable picture in the feedback... note however that feedback plots cannot reference inputs. */

/* STACK can also output JSON if need be and stackmap objects can be used in other places too, but are very inconvenient when dealing with modifications to deep structures. To find out more about what is available just check the "stackstrings.mac" file it defines the functions. */]]></text>
      </feedbackvariables>
      <node>
        <name>0</name>
        <description/>
        <answertest>AlgEquiv</answertest>
        <sans>ans1</sans>
        <tans>ans1</tans>
        <testoptions/>
        <quiet>0</quiet>
        <truescoremode>=</truescoremode>
        <truescore>1.0000000</truescore>
        <truepenalty/>
        <truenextnode>-1</truenextnode>
        <trueanswernote>prt1-1-T</trueanswernote>
        <truefeedback format="html">
          <text><![CDATA[<p>In the input we have {@if is(parsed=und) then 0 else length(parsed)@} points. By color (red/green/blue/black) {@red@}/{@green@}/{@blue@}/{@black@}.</p><p>The raw value of the input is <pre>{@ans1@}</pre></p>]]></text>
        </truefeedback>
        <falsescoremode>=</falsescoremode>
        <falsescore>0.0000000</falsescore>
        <falsepenalty/>
        <falsenextnode>-1</falsenextnode>
        <falseanswernote>prt1-1-F</falseanswernote>
        <falsefeedback format="html">
          <text/>
        </falsefeedback>
      </node>
    </prt>
  </question>
</quiz>
