File indexing completed on 2024-04-21 14:43:27

0001 /* GCompris - analog_electricity.js
0002  *
0003  * SPDX-FileCopyrightText: 2020 Aiswarya Kaitheri Kandoth <aiswaryakk29@gmail.com>
0004  *
0005  * Authors:
0006  *   Bruno Coudoin (GTK+ version)
0007  *   Aiswarya Kaitheri Kandoth <aiswaryakk29@gmail.com> (Qt Quick port)
0008  *   Timothée Giet <animtim@gmail.com> (Help for the Qt Quick port)
0009  *
0010  *   SPDX-License-Identifier: GPL-3.0-or-later
0011  */
0012 .pragma library
0013 .import QtQuick 2.12 as Quick
0014 .import "cktsim.js" as Engine
0015 .import "qrc:/gcompris/src/core/core.js" as Core
0016 
0017 var url = "qrc:/gcompris/src/activities/analog_electricity/resource/";
0018 var urlDigital = "qrc:/gcompris/src/activities/digital_electricity/resource/";
0019 
0020 var numberOfLevel;
0021 var items;
0022 var view;
0023 var toolDelete;
0024 var animationInProgress;
0025 var selectedIndex;
0026 var selectedTerminal;
0027 var components = [];
0028 var determiningComponents = [];
0029 var connectionCount = 0;
0030 var levelProperties;
0031 var invalidCircuit = false;
0032 var processingAnswer = false;
0033 var answerKeys = [];
0034 
0035 var uniqueID = 0;
0036 var netlistComponents = [];
0037 var vSourcesList = [];
0038 var netlist = [];
0039 
0040 var wireColors = [
0041     "#72559b",
0042     "#094386",
0043     "#97509e",
0044     "#2890d4",
0045     "#407a52",
0046     "#fee348",
0047     "#a3ca4b",
0048     "#ea6712",
0049     "#f4819a",
0050     "#b82c58",
0051     "#6b4a36",
0052     "#d42e2e",
0053     "#e9dfc9"
0054 ]
0055 var colorIndex = 0;
0056 
0057 var currentZoom;
0058 var maxZoom = 0.375;
0059 var minZoom = 0.125;
0060 var defaultZoom = 0.25;
0061 var zoomStep = 0.0625;
0062 //bool to avoid createNetList when deleting wires on stop function...
0063 var isStopped = false;
0064 
0065 var direction = {
0066     LEFT: -1,
0067     RIGHT: 1,
0068     UP: -2,
0069     DOWN: 2
0070 };
0071 
0072 var viewPort = {
0073     leftExtreme: 0,
0074     rightExtreme: 1,
0075     topExtreme: 0,
0076     bottomExtreme: 1,
0077     leftEdge: 0,
0078     topEdge: 0
0079 };
0080 
0081 function start(items_) {
0082     items = items_;
0083     numberOfLevel = items.tutorialDataset.tutorialLevels.length;
0084     items.currentLevel = Core.getInitialLevel(numberOfLevel);
0085     initLevel();
0086 }
0087 
0088 function reset() {
0089     stop();
0090     initLevel();
0091 }
0092 
0093 function stop() {
0094     isStopped = true;
0095     var nbOfComponents = components.length;
0096     for(var i = 0 ; i < nbOfComponents ; ++i) {
0097         removeComponent(0);
0098     }
0099 }
0100 
0101 function initLevel() {
0102     connectionCount = 0;
0103     netlistComponents = [];
0104     netlist = [];
0105     components = [];
0106     items.availablePieces.model.clear();
0107     items.availablePieces.view.currentDisplayedGroup = 0;
0108     items.availablePieces.view.previousNavigation = 1;
0109     items.availablePieces.view.nextNavigation = 1;
0110     determiningComponents = [];
0111     colorIndex = 0;
0112     animationInProgress = false;
0113     disableToolDelete();
0114     deselect();
0115     items.availablePieces.hideToolbar();
0116 
0117     currentZoom = defaultZoom;
0118     viewPort.leftEdge = 0;
0119     viewPort.topEdge = 0;
0120     items.playArea.x = items.mousePan.drag.maximumX;
0121     items.playArea.y = items.mousePan.drag.maximumY;
0122     isStopped = false;
0123 
0124 
0125     if (!items.isTutorialMode) {
0126         items.tutorialInstruction.index = -1;
0127         processingAnswer = false;
0128         loadFreeMode();
0129     } else {
0130         processingAnswer = false;
0131         levelProperties = items.tutorialDataset.tutorialLevels[items.currentLevel];
0132 
0133         for (var i = 0; i < levelProperties.inputComponentList.length; i++) {
0134             var currentInputComponent = levelProperties.inputComponentList[i];
0135             items.availablePieces.model.append( {
0136                 "imgName": currentInputComponent.imageName,
0137                 "componentSrc": currentInputComponent.componentSource,
0138                 "imgWidth": currentInputComponent.width,
0139                 "imgHeight": currentInputComponent.height,
0140                 "toolTipText": currentInputComponent.toolTipText
0141             });
0142         }
0143 
0144         for (var i = 0; i < levelProperties.playAreaComponentList.length; i++) {
0145             var index = components.length;
0146 
0147             var currentPlayAreaComponent = levelProperties.playAreaComponentList[i];
0148             var staticElectricalComponent = Qt.createComponent("qrc:/gcompris/src/activities/analog_electricity/components/" + currentPlayAreaComponent.componentSource);
0149 
0150             components[index] = staticElectricalComponent.createObject(
0151                         items.playArea, {
0152                             "componentIndex": index,
0153                             "posX": levelProperties.playAreaComponentPositionX[i] * currentZoom,
0154                             "posY": levelProperties.playAreaComponentPositionY[i] * currentZoom,
0155                             "imgWidth": currentPlayAreaComponent.width * currentZoom,
0156                             "imgHeight": currentPlayAreaComponent.height * currentZoom,
0157                             "destructible": false
0158                         });
0159             ++uniqueID;
0160             components[index].componentName = components[index].componentName + uniqueID.toString();
0161             components[index].initConnections();
0162         }
0163         deselect();
0164 
0165         var _determiningComponentsIndex = levelProperties.determiningComponentsIndex
0166         for (var i = 0; i < _determiningComponentsIndex.length; i++) {
0167             determiningComponents[i] = components[_determiningComponentsIndex[i]];
0168         }
0169 
0170         //create wires
0171         for (i = 0; i < levelProperties.wires.length; i++) {
0172             var terminalNumber = levelProperties.wires[i][1];
0173             var connectionPoint = components[levelProperties.wires[i][0]].connectionPoints.itemAt(terminalNumber);
0174             terminalPointSelected(connectionPoint, false);
0175 
0176             terminalNumber = levelProperties.wires[i][3];
0177             var terminalToConnect = components[levelProperties.wires[i][2]].connectionPoints.itemAt(terminalNumber);
0178             terminalPointSelected(terminalToConnect, false);
0179         }
0180 
0181         if (levelProperties.introMessage.length != 0) {
0182             items.tutorialInstruction.index = 0;
0183             items.tutorialInstruction.intro = levelProperties.introMessage;
0184         } else {
0185             items.tutorialInstruction.index = -1;
0186         }
0187     }
0188 }
0189 
0190 function loadFreeMode() {
0191     var componentList = items.tutorialDataset.componentList;
0192 
0193     for (var i = 0; i < componentList.length; i++) {
0194         items.availablePieces.model.append( {
0195             "imgName": componentList[i].imageName,
0196             "componentSrc": componentList[i].componentSource,
0197             "imgWidth": componentList[i].width,
0198             "imgHeight": componentList[i].height,
0199             "toolTipText": componentList[i].toolTipText,
0200         });
0201     }
0202 }
0203 
0204 function checkAnswer() {
0205     answerKeys = [];
0206 
0207     if(invalidCircuit){
0208         if(items.currentLevel === 1) {
0209         //special case for level 2 to teach voltage source loop
0210             items.bonus.good('gnu');
0211             processingAnswer = false;
0212             return;
0213         } else {
0214         //default case for all other levels
0215             items.bonus.bad('gnu', items.bonus.checkAnswer);
0216             processingAnswer = false;
0217             return;
0218         }
0219     }
0220 
0221     for(var i = 0; i < determiningComponents.length; ++i) {
0222         answerKeys[i] = determiningComponents[i].checkComponentAnswer();
0223         processingAnswer = true;
0224     }
0225 
0226     for(var i in answerKeys) {
0227         if(levelProperties.answerKey[i] === answerKeys[i] && processingAnswer) {
0228             items.bonus.good('gnu');
0229         } else {
0230             items.bonus.bad('gnu', items.bonus.checkAnswer);
0231             processingAnswer = false;
0232         }
0233     }
0234 }
0235 
0236 function zoomIn() {
0237     var previousZoom = currentZoom;
0238     currentZoom += zoomStep;
0239     if (currentZoom > maxZoom)
0240         currentZoom = maxZoom;
0241     var zoomRatio = currentZoom / previousZoom;
0242     updateComponentDimension(zoomRatio);
0243 
0244     if (currentZoom == maxZoom) {
0245         items.availablePieces.zoomInBtn.state = "cannotZoomIn";
0246     } else {
0247         items.availablePieces.zoomInBtn.state = "canZoomIn";
0248     }
0249     items.availablePieces.zoomOutBtn.state = "canZoomOut";
0250 
0251     if (items.zoomLvl < 0.5) {
0252         items.zoomLvl += 0.125;
0253     }
0254 }
0255 
0256 function zoomOut() {
0257     var previousZoom = currentZoom;
0258     currentZoom -= zoomStep;
0259     if (currentZoom < minZoom)
0260         currentZoom = minZoom;
0261     var zoomRatio = currentZoom / previousZoom;
0262     updateComponentDimension(zoomRatio);
0263 
0264     if (currentZoom == minZoom) {
0265         items.availablePieces.zoomOutBtn.state = "cannotZoomOut";
0266     } else {
0267         items.availablePieces.zoomOutBtn.state = "canZoomOut";
0268     }
0269     items.availablePieces.zoomInBtn.state = "canZoomIn";
0270 
0271     if (items.zoomLvl > 0) {
0272         items.zoomLvl -= 0.125;
0273     }
0274 }
0275 
0276 function updateComponentDimension(zoomRatio) {
0277     for (var i = 0; i < components.length; i++) {
0278         components[i].posX *= zoomRatio;
0279         components[i].posY *= zoomRatio;
0280         components[i].imgWidth *= zoomRatio;
0281         components[i].imgHeight *= zoomRatio;
0282         updateWires(i);
0283     }
0284 }
0285 
0286 function nextLevel() {
0287     items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel);
0288     reset();
0289 }
0290 
0291 function previousLevel() {
0292     items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel);
0293     reset();
0294 }
0295 
0296 function createComponent(x, y, componentIndex) {
0297     x = x / items.playArea.width;
0298     y = y / items.playArea.height;
0299 
0300     var index = components.length;
0301 
0302     var component = items.availablePieces.repeater.itemAt(componentIndex);
0303     var electricComponent = Qt.createComponent("qrc:/gcompris/src/activities/analog_electricity/components/" + component.source);
0304 
0305     components[index] = electricComponent.createObject(
0306                         items.playArea, {
0307                             "componentIndex": index,
0308                             "posX": x,
0309                             "posY": y,
0310                             "imgWidth": component.imageWidth * currentZoom,
0311                             "imgHeight": component.imageHeight * currentZoom,
0312                             "destructible": true
0313                         });
0314     ++uniqueID;
0315     components[index].componentName = components[index].componentName + uniqueID.toString();
0316     components[index].initConnections();
0317     deselect();
0318 }
0319 
0320 /* Creates wire between two points.
0321 */
0322 function terminalPointSelected(terminal, destructible) {
0323     if(destructible == undefined)
0324         destructible = true;
0325     if(selectedTerminal == -1 || selectedTerminal == terminal)
0326         selectedTerminal = terminal;
0327     else if(selectedTerminal.parent != terminal.parent) {
0328         var connectionPoint = terminal;
0329         createWire(connectionPoint, destructible);
0330         deselect();
0331     }
0332     else {
0333         deselect();
0334         selectedTerminal = terminal;
0335         terminal.selected = true;
0336     }
0337     disableToolDelete();
0338 }
0339 
0340 function disableToolDelete() {
0341     if(toolDelete == true) {
0342         toolDelete = false;
0343         items.availablePieces.toolDelete.state = "notSelected";
0344     }
0345 }
0346 
0347 function nextColorIndex() {
0348     if(colorIndex < wireColors.length - 1)
0349         ++colorIndex;
0350     else
0351         colorIndex = 0;
0352 }
0353 
0354 function createWire(connectionPoint, destructible) {
0355     var wireComponent = Qt.createComponent("qrc:/gcompris/src/activities/analog_electricity/Wire.qml");
0356     if(connectionPoint.wires.length === 0 && selectedTerminal.wires.length === 0) {
0357         connectionPoint.updateNetlistIndex(selectedTerminal.netlistIndex);
0358         selectedTerminal.updateNetlistIndex(selectedTerminal.netlistIndex);
0359         connectionPoint.colorIndex = colorIndex;
0360         selectedTerminal.colorIndex = colorIndex;
0361         nextColorIndex();
0362     } else {
0363         if(connectionPoint.wires.length > 0) {
0364             selectedTerminal.updateNetlistIndex(connectionPoint.netlistIndex, connectionPoint.colorIndex);
0365         }
0366         if(selectedTerminal.wires.length > 0) {
0367             connectionPoint.updateNetlistIndex(selectedTerminal.netlistIndex, selectedTerminal.colorIndex);
0368         }
0369     }
0370     var wire = wireComponent.createObject(
0371                items.playArea, {
0372                     "node1": selectedTerminal,
0373                     "node2": connectionPoint,
0374                     "destructible": destructible,
0375                 });
0376     connectionPoint.wires.push(wire);
0377     selectedTerminal.wires.push(wire);
0378     updateWires(connectionPoint.parent.componentIndex);
0379     updateWires(selectedTerminal.parent.componentIndex);
0380     connectionPoint.parent.checkConnections();
0381     selectedTerminal.parent.checkConnections();
0382     restartTimer();
0383 }
0384 
0385 function updateWires(index) {
0386     var component = components[index];
0387     if(component == undefined || component.noOfConnectionPoints == undefined)
0388         return;
0389 
0390     var rotatedAngle = component.initialAngle * Math.PI / 180;
0391     var rotatedAngleSin = Math.sin(rotatedAngle);
0392     var rotatedAngleCos = Math.cos(rotatedAngle);
0393     for(var i = 0 ; i < component.noOfConnectionPoints ; ++i) {
0394         var terminal = component.connectionPoints.itemAt(i);
0395         for(var j = 0 ; j < terminal.wires.length ; ++j) {
0396             var wire = terminal.wires[j];
0397             if(wire.node1 != terminal) {
0398                 var otherAngle = wire.node1.parent.initialAngle * Math.PI / 180;
0399                 var otherAngleCos = Math.cos(otherAngle);
0400                 var otherAngleSin = Math.sin(otherAngle);
0401                 var x = wire.node1.xCenterFromComponent;
0402                 var y = wire.node1.yCenterFromComponent;
0403                 var x1 = wire.node1.xCenter - x + x * otherAngleCos - y * otherAngleSin;
0404                 var y1 = wire.node1.yCenter - y + x * otherAngleSin + y * otherAngleCos;
0405 
0406                 x = terminal.xCenterFromComponent;
0407                 y = terminal.yCenterFromComponent;
0408                 var x2 = terminal.xCenter - x + x * rotatedAngleCos - y * rotatedAngleSin;
0409                 var y2 = terminal.yCenter - y + x * rotatedAngleSin + y * rotatedAngleCos;
0410 
0411                 var width = Math.pow((Math.pow(x1 - x2, 2) +  Math.pow(y1 - y2, 2)),0.5) + 2;
0412                 var angle = (180/Math.PI)*Math.atan((y2-y1)/(x2-x1));
0413                 if(x2 - x1 < 0)
0414                     angle = angle - 180;
0415                 wire.x = x1;
0416                 wire.y = y1 - wire.height / 2;
0417                 wire.width = width;
0418                 wire.rotation = angle;
0419             }
0420         }
0421     }
0422     for(var i = 0 ; i < component.noOfConnectionPoints ; ++i) {
0423         var terminal = component.connectionPoints.itemAt(i);
0424         for(var j = 0 ; j < terminal.wires.length ; ++j) {
0425             var x = terminal.xCenterFromComponent;
0426             var y = terminal.yCenterFromComponent;
0427             var x1 = terminal.xCenter - x + x * rotatedAngleCos - y * rotatedAngleSin;
0428             var y1 = terminal.yCenter - y + x * rotatedAngleSin + y * rotatedAngleCos;
0429 
0430             var wire = terminal.wires[j];
0431             if(wire.node2 != terminal) {
0432                 var otherAngle = wire.node2.parent.initialAngle * Math.PI / 180;
0433                 var otherAngleCos = Math.cos(otherAngle);
0434                 var otherAngleSin = Math.sin(otherAngle);
0435                 x = wire.node2.xCenterFromComponent;
0436                 y = wire.node2.yCenterFromComponent;
0437                 var x2 = wire.node2.xCenter - x + x * otherAngleCos - y * otherAngleSin;
0438                 var y2 = wire.node2.yCenter - y + x * otherAngleSin + y * otherAngleCos;
0439 
0440                 var width = Math.pow((Math.pow(x1 - x2, 2) +  Math.pow(y1 - y2, 2)),0.5) + 2;
0441                 var angle = (180/Math.PI)*Math.atan((y2-y1)/(x2-x1));
0442                 if(x2 - x1 < 0)
0443                     angle = angle - 180;
0444                 wire.x = x1;
0445                 wire.y = y1;
0446                 wire.width = width;
0447                 wire.rotation = angle;
0448             }
0449         }
0450     }
0451 }
0452 
0453 function updateWiresOnResize() {
0454     for(var i = 0; i < components.length; ++i) {
0455         updateWires(i);
0456     }
0457 }
0458 
0459 function deselect() {
0460     items.availablePieces.rotateLeft.state = "canNotBeSelected";
0461     items.availablePieces.rotateRight.state = "canNotBeSelected";
0462     items.availablePieces.info.state = "canNotBeSelected";
0463     items.infoTxt.visible = false
0464     selectedIndex = -1;
0465     selectedTerminal = -1;
0466     for(var i = 0 ; i < components.length ; ++i) {
0467         var component = components[i];
0468         for(var j = 0 ; j < component.noOfConnectionPoints ; ++j)
0469             component.connectionPoints.itemAt(j).selected = false;
0470     }
0471 }
0472 
0473 function removeComponent(index) {
0474     var component = components[index];
0475     for(var i = 0 ; i < component.noOfConnectionPoints ; ++i) {
0476         var terminal = component.connectionPoints.itemAt(i);
0477         if(terminal.wires.length != 0) {
0478             var wiresLength = terminal.wires.length;
0479             for(var j = 0; j < wiresLength; ++j) {
0480                 removeWire(terminal.wires[0]);
0481             }
0482         }
0483     }
0484 
0485     components[index].destroy();
0486     components.splice(index, 1);
0487 
0488     for(var i = index; i < components.length; ++i) {
0489         --components[i].componentIndex;
0490     }
0491     deselect();
0492 }
0493 
0494 function removeWire(wire) {
0495     var connectionPoint1 = wire.node1;
0496     var connectionPoint2 = wire.node2;
0497 
0498     var removeIndex = connectionPoint1.wires.indexOf(wire);
0499     connectionPoint1.wires.splice(removeIndex, 1);
0500     removeIndex = connectionPoint2.wires.indexOf(wire);
0501     connectionPoint2.wires.splice(removeIndex, 1);
0502 
0503     wire.destroy();
0504     deselect();
0505 
0506     if(connectionPoint1.wires.length === 0) {
0507         connectionPoint1.resetIndex();
0508     } else {
0509         ++connectionCount;
0510         connectionPoint1.updateNetlistIndex(connectionCount, colorIndex);
0511         nextColorIndex();
0512     }
0513     if(connectionPoint2.wires.length === 0) {
0514         connectionPoint2.resetIndex();
0515     } else {
0516         ++connectionCount;
0517         connectionPoint2.updateNetlistIndex(connectionCount, colorIndex);
0518         nextColorIndex();
0519     }
0520     connectionPoint1.parent.checkConnections();
0521     connectionPoint2.parent.checkConnections();
0522 
0523     if(!isStopped)
0524         restartTimer();
0525 }
0526 
0527 function componentSelected(index) {
0528     selectedIndex = index;
0529     items.availablePieces.rotateLeft.state = "canBeSelected";
0530     items.availablePieces.rotateRight.state = "canBeSelected";
0531     items.availablePieces.info.state = "canBeSelected";
0532 }
0533 
0534 function rotateLeft() {
0535     components[selectedIndex].rotationAngle = -2;
0536     components[selectedIndex].rotateComponent.start();
0537 }
0538 
0539 function rotateRight() {
0540     components[selectedIndex].rotationAngle = 2;
0541     components[selectedIndex].rotateComponent.start();
0542 }
0543 
0544 function displayInfo() {
0545     var component = components[selectedIndex];
0546     deselect();
0547     items.infoTxt.visible = true;
0548     items.infoTxt.text = component.information;
0549 
0550     if(component.infoImageSrc != undefined) {
0551         items.infoImage.imgVisible = true;
0552         items.infoImage.source = url + component.infoImageSrc;
0553     }
0554     else {
0555         items.infoImage.imgVisible = false;
0556         items.infoImage.source = "";
0557     }
0558 }
0559 
0560 function updateToolTip(toolTipText) {
0561     items.toolTip.show(toolTipText);
0562 }
0563 
0564 function restartTimer() {
0565     items.netlistTimer.restart();
0566 }
0567 
0568 function createNetlist() {
0569     netlist.length = 0;
0570     netlistComponents.length = 0;
0571     vSourcesList.length = 0;
0572     for(var i = 0; i < components.length; i++) {
0573         var component = components[i];
0574         component.addToNetlist();
0575     }
0576     dcAnalysis();
0577 }
0578 
0579 function dcAnalysis() {
0580     var ckt = new Engine.cktsim.Circuit();
0581     ckt.load_netlist(netlist);
0582     var voltageResults = ckt.dc();
0583     if(ckt.GCWarning != "") {
0584         displayWarning(ckt.GCWarning);
0585         return;
0586     } else {
0587         invalidCircuit = false;
0588     }
0589 
0590     var currentResults = ckt.GCCurrentResults;
0591     for(var i in currentResults) {
0592         if(vSourcesList[i] != undefined)
0593             vSourcesList[i].current = currentResults[i];
0594     }
0595 
0596     for(var i in netlistComponents) {
0597         if(netlistComponents[i].nodeVoltages == undefined) {
0598             continue;
0599         } else {
0600             for(var j = 0 ; j < netlistComponents[i].nodeVoltages.length ; j++) {
0601                 if(netlistComponents[i].internalNetlistIndex != null) {
0602                     netlistComponents[i].nodeVoltages[j] = voltageResults[netlistComponents[i].internalNetlistIndex[j]];
0603                 } else {
0604                     netlistComponents[i].nodeVoltages[j] = voltageResults[netlistComponents[i].externalNetlistIndex[j]];
0605                 }
0606             }
0607             netlistComponents[i].updateValues();
0608         }
0609     }
0610 }
0611 
0612 function displayWarning(message_) {
0613     items.availablePieces.hideToolbar();
0614     items.infoTxt.visible = true;
0615     items.infoTxt.text = message_;
0616     invalidCircuit = true;
0617 }