diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 894a44c..4ab03d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ +*.pyc *.py[cod] *$py.class @@ -24,7 +25,6 @@ wheels/ .installed.cfg *.egg MANIFEST - # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -102,3 +102,10 @@ venv.bak/ # mypy .mypy_cache/ + +# Misc +*.swp +compiledUI/ + +# editor configs +.vscode \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..da51df6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at ayhamaboualfadl@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..0e1971c --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1 @@ +
realaltffour, [Profile](https://api.github.com/users/realaltffour). Made {counter} commits.

AliAlboainin96, [Profile](https://api.github.com/users/AliAlboainin96). Made {counter} commits.

TriptSharma, [Profile](https://api.github.com/users/TriptSharma). Made {counter} commits.

README1ST, [Profile](https://api.github.com/users/README1ST). Made {counter} commits.

xypnox, [Profile](https://api.github.com/users/xypnox). Made {counter} commits.

bjornwelboren, [Profile](https://api.github.com/users/bjornwelboren). Made {counter} commits.

GiuB, [Profile](https://api.github.com/users/GiuB). Made {counter} commits.

omikhailk, [Profile](https://api.github.com/users/omikhailk). Made {counter} commits.
\ No newline at end of file diff --git a/README.md b/README.md index 9ca3b79..8408c80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # GraphSolver -An application for solving linear, quadratic graphs. +An application for solving linear/quadratic graphs. diff --git a/app.py b/app.py new file mode 100644 index 0000000..0a51d24 --- /dev/null +++ b/app.py @@ -0,0 +1,13 @@ +from PyQt5 import QtWidgets +from ui.mainWindow import mainWin + + +def main(): + app = QtWidgets.QApplication([]) + window = mainWin() + app.exec_() + exit(0) + + +if __name__ == '__main__': + main() diff --git a/buildUI.py b/buildUI.py new file mode 100644 index 0000000..320041b --- /dev/null +++ b/buildUI.py @@ -0,0 +1,15 @@ +import os +from os.path import isfile, join +from os import listdir + +cwd = os.getcwd() + "/" + +ui_files = [f for f in listdir("ui/") if isfile(join("ui/", f))] + +ui_py = {x.replace('.ui', '.py') for x in ui_files} + +if not os.path.exists("compiledUI"): + os.makedirs("compiledUI") + +for ui, uipy in zip(ui_files, ui_py): + os.system("pyuic5 " + cwd + "ui/" + ui + " -o" + " compiledUI/" + uipy) diff --git a/clean.sh b/clean.sh new file mode 100644 index 0000000..bcf990d --- /dev/null +++ b/clean.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -rf compiledUI diff --git a/commits.py b/commits.py new file mode 100644 index 0000000..2ffab37 --- /dev/null +++ b/commits.py @@ -0,0 +1,31 @@ +import requests + + +def get_user_info(): + URL = "https://api.github.com/repos/realaltffour/GraphSolver/contributors" + r = requests.get(URL) + r = r.json() + + for contributor in r: + get_user_commits(contributor['login'], contributor['url']) + + +def get_user_commits(login, github_profile): + URL = "https://api.github.com/repos/realaltffour/GraphSolver/commits" + # Filter by login + r = requests.get(URL, {'author': login}) + r = r.json() + + with open('CONTRIBUTORS.md', 'a') as contributor_file: + counter = 0 + for commits_by_user in r: + counter += 1 + contributor_file.write( + f"
{login}, [Profile]({github_profile})." + " Made {counter} commits.
" + ) + + contributor_file.close() + + +get_user_info() diff --git a/methods/linear/calcLinear.py b/methods/linear/calcLinear.py new file mode 100644 index 0000000..466e0a2 --- /dev/null +++ b/methods/linear/calcLinear.py @@ -0,0 +1,33 @@ +import math + + +class Linear: + def __init__(self, a, c, length): + self.a = a + self.c = c + self.length = length + + def calc_linear_line(self): + return [(0 - self.length, self.a * (0 - self.length)), + (0 - self.length, self.a * self.length)] + + def calcLinearIntersection(self, line): + """ + This function accepts one line of Linear class + and returns the coordinates of intersection + between the self and the line passed + + If there is no intercept it returns 0 + If there are infinite intercepts it return math.inf + If there is one unique intercept it returns a tuple with (x, y) + """ + + if self.a == line.a: + if self.c == line.c: + return math.inf + return 0 + + x = (line.c - self.c) / (self.a - line.a) + y = self.a * (line.c - self.c) / (self.a - line.a) + self.c + + return (x, y) diff --git a/methods/linear/calcLinearIntercept.py b/methods/linear/calcLinearIntercept.py new file mode 100644 index 0000000..bff1524 --- /dev/null +++ b/methods/linear/calcLinearIntercept.py @@ -0,0 +1,28 @@ +import math + + +class LinearIntercept: + def __init__(self, a, c): + # Line eq: y = ax + c + self.a = a + self.c = c + + def calcLinearIntersection(self, line): + """ + This function accepts two lines of Linear class + and returns the coordinates of intersection between the two lines + + If there is no intercept it returns 0 + If there are infinite intercepts it return math.inf + If there is one unique intercept it returns a tuple with (x, y) + """ + + if self.a == line.a: + if self.c == line.c: + return math.inf + return 0 + + x = (line.c-self.c)/(self.a - line.a) + y = self.a*(line.c - self.c)/(self.a - line.a) + self.c + + return (x, y) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1b1a301 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ + +PyQt5==5.13.1 +requests==2.22.0 +======= +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * +from linear import * +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import sys + diff --git a/setup.py b/setup.py index 505efa9..b1fa006 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,19 @@ -from setuptools import setup +from setuptools import setup, find_packages + +with open('README.md') as f: + readme = f.read() + +with open('LICENSE') as f: + license = f.read() setup( - name='GraphSolver', - version='1.0', - description='Algebric equation solver displayed on graph.', - author='Ali, Ayham', - packages=['PyAlgebra'] + name='GraphSolver', + version='0.0.1', + description='An application for solving linear/quadratic graphs.', + long_description=readme, + author='altf_four', + author_email='ayhamaboualfadl@gmail.com', + url='https://github.com/realaltffour/GraphSolver', + license=license, + packages=find_packages(exclude=('tests', 'docs')) ) diff --git a/ui/linearInterceptWindow.py b/ui/linearInterceptWindow.py new file mode 100644 index 0000000..14e7217 --- /dev/null +++ b/ui/linearInterceptWindow.py @@ -0,0 +1,38 @@ +from PyQt5 import QtWidgets, uic + + +class linearInterceptWin(QtWidgets.QMainWindow): + def __init__(self): + super(linearInterceptWin, self).__init__() + self.ui = uic.loadUi("ui/linearInterceptWindow.ui", self) + + # Get calcBtn, and connect it to stuff. + self.calcBtn = self.findChild(QtWidgets.QPushButton, "calcBtn") + self.calcBtn.clicked.connect(self.calcBtnPressed) + + # Get line1mBox + self.line1mBox = self.findChild(QtWidgets.QLineEdit, "line1mBox") + # Get line1cBox + self.line1cBox = self.findChild(QtWidgets.QLineEdit, "line1cBox") + + # Get line2mBox + self.line2mBox = self.findChild(QtWidgets.QLineEdit, "line2mBox") + # Get line2cBox + self.line2cBox = self.findChild(QtWidgets.QLineEdit, "line2cBox") + + # Get resultXBox + self.resultXBox = self.findChild(QtWidgets.QLineEdit, "resultXBox") + # Get resultYBox + self.resultYBox = self.findChild(QtWidgets.QLineEdit, "resultYBox") + + self.ui.show() + + def calcBtnPressed(self): + m1 = int(self.line1mBox.text()) + c1 = int(self.line1cBox.text()) + m2 = int(self.line2mBox.text()) + c2 = int(self.line2cBox.text()) + x = (c2 - c1) / (m1 - m2) + y = m1 * x + c1 + self.resultXBox.setText(str(x)) + self.resultYBox.setText(str(y)) diff --git a/ui/linearInterceptWindow.ui b/ui/linearInterceptWindow.ui new file mode 100644 index 0000000..30d0c5c --- /dev/null +++ b/ui/linearInterceptWindow.ui @@ -0,0 +1,158 @@ + + + linearInterceptWin + + + + 0 + 0 + 399 + 158 + + + + MainWindow + + + + + + 0 + 0 + 391 + 121 + + + + + + + Line 1 + + + + + 0 + 20 + 167 + 58 + + + + + + + m: + + + + + + + + + + c: + + + + + + + + + + + + + + Line 2 + + + + + 0 + 20 + 167 + 58 + + + + + + + m: + + + + + + + + + + c: + + + + + + + + + + + + + + Calculate + + + + + + + + + 30 + 120 + 331 + 27 + + + + + + + X: + + + + + + + true + + + + + + + Y: + + + + + + + true + + + + + + + + + + diff --git a/ui/linearWindow.py b/ui/linearWindow.py new file mode 100644 index 0000000..c1155d6 --- /dev/null +++ b/ui/linearWindow.py @@ -0,0 +1,35 @@ +from PyQt5 import QtWidgets, uic +import matplotlib.pyplot as plt +import numpy as np + + +class linearWin(QtWidgets.QMainWindow): + m = 0.0 + c = 0.0 + + def __init__(self): + super(linearWin, self).__init__() + self.ui = uic.loadUi("ui/linearWindow.ui", self) + + # Get calcBtn, and connect it to stuff. + self.calcBtn = self.findChild(QtWidgets.QPushButton, "calcBtn") + self.calcBtn.clicked.connect(self.calcBtnPressed) + + # Get mBox, and connect it to stuff. + self.mBox = self.findChild(QtWidgets.QLineEdit, "mBox") + # Get cBox, and connect it to stuff. + self.cBox = self.findChild(QtWidgets.QLineEdit, "cBox") + + self.ui.show() + + def calcBtnPressed(self): + m = int(self.mBox.text()) + c = int(self.cBox.text()) + x = np.linspace(-5, 5, 100) + y = m * x + c + plt.plot(x, y, '-r', label='y=' + str(m) + 'x' + '+' + str(c)) + plt.title("Graph of y=" + str(m) + "x" + "+" + str(c)) + plt.xlabel('x', color='#1C2833') + plt.ylabel('y', color='#1C2833') + plt.legend(loc="upper left") + plt.show() diff --git a/ui/linearWindow.ui b/ui/linearWindow.ui new file mode 100644 index 0000000..99713e3 --- /dev/null +++ b/ui/linearWindow.ui @@ -0,0 +1,91 @@ + + + linearWin + + + + 0 + 0 + 165 + 140 + + + + Linear + + + + + + 30 + 50 + 121 + 20 + + + + + + + 30 + 10 + 121 + 20 + + + + + + + 4 + 10 + 31 + 20 + + + + + 16 + + + + m: + + + + + + 4 + 50 + 31 + 21 + + + + + 16 + + + + c: + + + + + + 30 + 90 + 91 + 21 + + + + Calculate + + + + + + + + diff --git a/ui/mainWindow.py b/ui/mainWindow.py new file mode 100644 index 0000000..7d67733 --- /dev/null +++ b/ui/mainWindow.py @@ -0,0 +1,37 @@ +from PyQt5 import QtWidgets, uic +from ui.linearWindow import linearWin +from ui.linearInterceptWindow import linearInterceptWin +from ui.quadraticWindow import quadraticWin +from ui.quadraticInterceptWindow import quadraticInterceptWin + + +class mainWin(QtWidgets.QMainWindow): + equval = "Linear" + + def __init__(self): + super(mainWin, self).__init__() + self.ui = uic.loadUi("ui/mainWindow.ui", self) + + # Get runBtn, and connect it to stuff. + self.runBtn = self.findChild(QtWidgets.QPushButton, "runBtn") + self.runBtn.clicked.connect(self.runBtnPressed) + + # Get equCbo, and connect it to stuff. + self.equCbo = self.findChild(QtWidgets.QComboBox, "equCbo") + self.equCbo.activated[str].connect(self.onEquCboChanged) + + self.ui.show() + + def runBtnPressed(self): + print(self.equval) + if self.equval == "Linear": + linearWin() + elif self.equval == "LinearIntercept": + linearInterceptWin() + elif self.equval == "Quadratic": + quadraticWin() + elif self.equval == "QuadraticIntercept": + quadraticInterceptWin() + + def onEquCboChanged(self, text): + self.equval = text diff --git a/ui/mainWindow.ui b/ui/mainWindow.ui new file mode 100644 index 0000000..1f03c3e --- /dev/null +++ b/ui/mainWindow.ui @@ -0,0 +1,94 @@ + + + MainWindow + + + + 0 + 0 + 251 + 200 + + + + + 0 + 0 + + + + + 265 + 225 + + + + GraphSolver + + + + ../Icons/bar-graph-on-a-rectangle.png../Icons/bar-graph-on-a-rectangle.png + + + + + + 30 + 70 + 191 + 31 + + + + + 14 + + + + + Linear + + + + + LinearIntercept + + + + + Quadratic + + + + + QuadraticIntercept + + + + + + + 80 + 110 + 100 + 25 + + + + Run + + + true + + + false + + + false + + + + + + + diff --git a/ui/quadraticInterceptWindow.py b/ui/quadraticInterceptWindow.py new file mode 100644 index 0000000..8fb4f4a --- /dev/null +++ b/ui/quadraticInterceptWindow.py @@ -0,0 +1,77 @@ +from PyQt5 import QtWidgets, uic +import math + + +class quadraticInterceptWin(QtWidgets.QMainWindow): + def __init__(self): + super(quadraticInterceptWin, self).__init__() + self.ui = uic.loadUi("ui/quadraticInterceptWindow.ui", self) + + # Get calcBtn, and connect it to stuff. + self.calcBtn = self.findChild(QtWidgets.QPushButton, "calcBtn") + self.calcBtn.clicked.connect(self.calcBtnPressed) + + # Get line1aBox + self.line1aBox = self.findChild(QtWidgets.QLineEdit, "line1aBox") + # Get line1bBox + self.line1bBox = self.findChild(QtWidgets.QLineEdit, "line1bBox") + # Get line1cBox + self.line1cBox = self.findChild(QtWidgets.QLineEdit, "line1cBox") + + # Get line2aBox + self.line2aBox = self.findChild(QtWidgets.QLineEdit, "line2aBox") + # Get line2bBox + self.line2bBox = self.findChild(QtWidgets.QLineEdit, "line2bBox") + # Get line2cBox + self.line2cBox = self.findChild(QtWidgets.QLineEdit, "line2cBox") + + # Get resultBox + self.resultBox = self.findChild(QtWidgets.QTextEdit, "resultBox") + + self.ui.show() + + def calcBtnPressed(self): + a1 = int(self.line1aBox.text()) + b1 = int(self.line1bBox.text()) + c1 = int(self.line1cBox.text()) + a2 = int(self.line2aBox.text()) + b2 = int(self.line2bBox.text()) + c2 = int(self.line2cBox.text()) + + a = a2 - a1 + b = b2 - b1 + c = c2 - c1 + + if a == 0.0 and b == 0.0 and c == 0.0: + self.resultBox.append( + "These Two parabolas are the same \n All points interset.") + return + + if a == 0.0: + if b == 0.0: + self.resultBox.append("These two parabolas do not intersect.") + return + else: + x1 = -c / b + y1 = a1 * x1 * x1 + b1 * x1 + c1 + self.resutlBox.append("One Intersect: \n (x: " + str(x1) + + ", y:" + str(y1)) + return + discriminant = b * b - 4 * a * c + + if discriminant < 0.0: + self.resutlBox.append("These two parabolas do not intersect.") + return + elif discriminant == 0.0: + x1 = -b / (2 * a) + y1 = a1 * x1 * x1 + b1 * x1 + c1 + self.resutlBox.append("One Intersect: \n (x: " + str(x1) + ", y:" + + str(y1)) + else: + x1 = (-b + math.sqrt(discriminant)) / (2 * a) + y1 = a1 * x1 * x1 + b1 * x1 + c1 + x2 = (-b - math.sqrt(discriminant)) / (2 * a) + y2 = a1 * x2 * x2 + b1 * x2 + c1 + self.resutlBox.append("Two Intersect: \n 1. (x: " + str(x1) + + ", y: " + str(y1) + "\n 2. (x: " + str(x2) + + ", y: " + str(y2)) diff --git a/ui/quadraticInterceptWindow.ui b/ui/quadraticInterceptWindow.ui new file mode 100644 index 0000000..348b621 --- /dev/null +++ b/ui/quadraticInterceptWindow.ui @@ -0,0 +1,155 @@ + + + quadraticInterceptWin + + + + 0 + 0 + 386 + 266 + + + + Quadratic Intercept + + + + + + 0 + 10 + 381 + 161 + + + + + + + + + Line 1 + + + + + 10 + 30 + 163 + 89 + + + + + + + a: + + + + + + + + + + b: + + + + + + + + + + c: + + + + + + + + + + + + + + Line 2 + + + + + 10 + 30 + 163 + 89 + + + + + + + a: + + + + + + + + + + b: + + + + + + + + + + c: + + + + + + + + + + + + + + + + Calculate + + + + + + + + + 40 + 180 + 301 + 70 + + + + true + + + + + + + diff --git a/ui/quadraticWindow.py b/ui/quadraticWindow.py new file mode 100644 index 0000000..463aaa2 --- /dev/null +++ b/ui/quadraticWindow.py @@ -0,0 +1,36 @@ +from PyQt5 import QtWidgets, uic +import matplotlib.pyplot as plt +import numpy as np + + +class quadraticWin(QtWidgets.QMainWindow): + def __init__(self): + super(quadraticWin, self).__init__() + self.ui = uic.loadUi("ui/quadraticWindow.ui", self) + + # Get calcBtn, and connect it to stuff. + self.calcBtn = self.findChild(QtWidgets.QPushButton, "calcBtn") + self.calcBtn.clicked.connect(self.calcBtnPressed) + + # Get aBox, and connect it to stuff. + self.aBox = self.findChild(QtWidgets.QLineEdit, "aBox") + # Get bBox, and connect it to stuff. + self.bBox = self.findChild(QtWidgets.QLineEdit, "bBox") + # Get cBox, and connect it to stuff. + self.cBox = self.findChild(QtWidgets.QLineEdit, "cBox") + + self.ui.show() + + def calcBtnPressed(self): + a = int(self.aBox.text()) + b = int(self.bBox.text()) + c = int(self.cBox.text()) + x = np.linspace(-5, 5, 100) + y = a * x**2 + b * x + c + graph = str(a) + 'x^2 + ' + str(b) + '*x + ' + str(c) + plt.plot(x, y, '-r', label=graph) + plt.title("Graph of " + graph) + plt.xlabel('x', color='#1C2833') + plt.ylabel('y', color='#1C2833') + plt.legend(loc="upper left") + plt.show() diff --git a/ui/quadraticWindow.ui b/ui/quadraticWindow.ui new file mode 100644 index 0000000..6e3289b --- /dev/null +++ b/ui/quadraticWindow.ui @@ -0,0 +1,74 @@ + + + quadraticWindow + + + + 0 + 0 + 187 + 164 + + + + Quadratic Line + + + + + + 10 + 10 + 165 + 145 + + + + + + + + + + + + b: + + + + + + + c: + + + + + + + + + + a: + + + + + + + + + + + + Calculate + + + + + + + + + +