Library - Code

From RoboWiki
Jump to: navigation, search

Return back to project page: Avalanche Cannon - Jakub Vojtek

Python code for the Web app library:

from flask import Flask, render_template, request, Response
from flask_socketio import SocketIO
from bs4 import BeautifulSoup
from blinker import signal
import cv2

app = Flask(__name__)
socketio = SocketIO(app)

button_click_signal = signal('button-click')
button_hold_signal = signal('button-hold')
image_click_signal = signal('image-click')
textfield_change_signal = signal('textfield-change')

file = None
video_feed_path = None

css = False
css_file_path = None


def run_app(html_file_path, host='0.0.0.0', port=5000):
    global file
    file = html_file_path
    socketio.run(app, allow_unsafe_werkzeug=True, host=host, port=port, debug=True)


def read_html_file(file_path):
    with open(file_path, 'r') as file:
        html_content = file.read()
    return html_content


def add_event_listeners(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')

    buttons = soup.find_all('button', {'id': True})

    for button in buttons:
        button_id = button['id']
        button['onmousedown'] = f"sendButtonHold('{button_id}')"
        button['onmouseup'] = f"sendButtonClick('{button_id}')"

    text_fields = soup.find_all('input', {'type': 'text', 'id': True})

    for text_field in text_fields:
        text_field_id = text_field['id']
        text_field['onchange'] = f"sendTextFieldContent('{text_field_id}', this.value)"

    images = soup.find_all('img', {'id': True})

    for img in images:
        img_id = img['id']
        img['onclick'] = f"sendImageClick(event, '{img_id}')"

    script_tag = soup.new_tag('script')
    script_tag.string = """
    function sendTextFieldContent(textFieldId, content) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/text_field_content', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({textFieldId: textFieldId, content: content}));
    };
    function sendButtonClick(buttonId) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/button_click', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({buttonId: buttonId}));
    };
    function sendButtonHold(buttonId) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/button_hold', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({buttonId: buttonId}));
    };
    function sendImageClick(event, imgId) {
        var img = document.getElementById(imgId);
        var rect = img.getBoundingClientRect();
        var x = Math.round(event.clientX - rect.left);
        var y = Math.round(event.clientY - rect.top);
        var position = "(" + x + ", " + y + ")";
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/image_click', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({imgId: imgId, position: position}));
    }
    """
    soup.head.append(script_tag)

    canvases = soup.find_all('canvas')

    for canvas in canvases:
        canvas_id = canvas['id']

        container_div = soup.new_tag('div')
        container_div['class'] = 'canvas-container'

        canvas.insert_before(container_div)

        container_div.append(canvas)

        buttons_div = soup.new_tag('div')
        buttons_div['class'] = 'canvas-buttons'

        # create buttons for drawing functionalities
        free_draw_btn = soup.new_tag('button')
        free_draw_btn['id'] = f'{canvas_id}-freeDrawBtn'
        free_draw_btn.string = 'Free Draw'
        buttons_div.append(free_draw_btn)

        line_draw_btn = soup.new_tag('button')
        line_draw_btn['id'] = f'{canvas_id}-lineDrawBtn'
        line_draw_btn.string = 'Draw Line'
        buttons_div.append(line_draw_btn)

        rectangle_draw_btn = soup.new_tag('button')
        rectangle_draw_btn['id'] = f'{canvas_id}-rectangleDrawBtn'
        rectangle_draw_btn.string = 'Draw Rectangle'
        buttons_div.append(rectangle_draw_btn)

        filled_rectangle_draw_btn = soup.new_tag('button')
        filled_rectangle_draw_btn['id'] = f'{canvas_id}-filledRectangleDrawBtn'
        filled_rectangle_draw_btn.string = 'Draw Filled Rectangle'
        buttons_div.append(filled_rectangle_draw_btn)

        oval_draw_btn = soup.new_tag('button')
        oval_draw_btn['id'] = f'{canvas_id}-ovalDrawBtn'
        oval_draw_btn.string = 'Draw Oval'
        buttons_div.append(oval_draw_btn)

        filled_oval_draw_btn = soup.new_tag('button')
        filled_oval_draw_btn['id'] = f'{canvas_id}-filledOvalDrawBtn'
        filled_oval_draw_btn.string = 'Draw Filled Oval'
        buttons_div.append(filled_oval_draw_btn)

        clear_btn = soup.new_tag('button')
        clear_btn['id'] = f'{canvas_id}-clearBtn'
        clear_btn.string = 'Clear'
        buttons_div.append(clear_btn)

        container_div.append(buttons_div)

        socket_script_tag = soup.new_tag('script',
                                         src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js",
                                         integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==",
                                         crossorigin="anonymous")
        soup.head.append(socket_script_tag)

        # javascript code for drawing functionalities
        canvas_script = f"""
        <script type="text/javascript" charset="utf-8">
            var socket = io();
            socket.on('connect', function() {{
                console.log("socket connected");
            }});
            var canvas = document.getElementById("{canvas_id}");
            var ctx_{canvas_id} = canvas.getContext("2d"); 
            var selectedMode = "freeDraw";
            var startPoint = null;
            var isDrawing = false;
            var lastX = 0;
            var lastY = 0;

            document.getElementById("{canvas_id}-freeDrawBtn").addEventListener("click", function() {{
                selectedMode = "freeDraw";
                startPoint = null; 
            }});

            document.getElementById("{canvas_id}-lineDrawBtn").addEventListener("click", function() {{
                selectedMode = "drawLine";
                startPoint = null;
            }});

            document.getElementById("{canvas_id}-rectangleDrawBtn").addEventListener("click", function() {{
                selectedMode = "drawRectangle";
                startPoint = null;
            }});

            document.getElementById("{canvas_id}-filledRectangleDrawBtn").addEventListener("click", function() {{
                selectedMode = "drawFilledRectangle";
                startPoint = null; 
            }});

            document.getElementById("{canvas_id}-ovalDrawBtn").addEventListener("click", function() {{
                selectedMode = "drawOval";
                startPoint = null; 
            }});

            document.getElementById("{canvas_id}-filledOvalDrawBtn").addEventListener("click", function() {{
                selectedMode = "drawFilledOval";
                startPoint = null; 
            }});

            document.getElementById("{canvas_id}-clearBtn").addEventListener("click", function() {{
                var canvas = document.getElementById("{canvas_id}");
                var ctx = canvas.getContext("2d");
                ctx.clearRect(0, 0, canvas.width, canvas.height); 
                selectedMode = "freeDraw"; 
            }});

            canvas.addEventListener("mousedown", function(e) {{
                if (selectedMode === "freeDraw") {{
                    isDrawing = true;
                    [lastX, lastY] = [e.offsetX, e.offsetY];
                }}
            }});

            canvas.addEventListener("mousemove", function(e) {{
                if (isDrawing && selectedMode === "freeDraw") {{
                    ctx_{canvas_id}.beginPath();
                    ctx_{canvas_id}.moveTo(lastX, lastY);
                    ctx_{canvas_id}.lineTo(e.offsetX, e.offsetY);
                    ctx_{canvas_id}.strokeStyle = "black";
                    ctx_{canvas_id}.lineWidth = 2;
                    ctx_{canvas_id}.stroke();
                    [lastX, lastY] = [e.offsetX, e.offsetY];
                }}
            }});

            canvas.addEventListener("mouseup", function() {{
                isDrawing = false;
            }});

            canvas.addEventListener("mouseout", function() {{
                isDrawing = false;
            }});

            canvas.addEventListener("click", function(e) {{
                if (selectedMode === "drawLine") {{
                    if (!startPoint) {{
                        startPoint = {{ x: e.offsetX, y: e.offsetY }};
                    }} else {{
                        drawLine(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
                        startPoint = null; 
                    }}
                }} else if (selectedMode === "drawRectangle") {{
                    if (!startPoint) {{
                        startPoint = {{ x: e.offsetX, y: e.offsetY }};
                    }} else {{
                        drawRectangle(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
                        startPoint = null;
                    }}
                }} else if (selectedMode === "drawFilledRectangle") {{
                    if (!startPoint) {{
                        startPoint = {{ x: e.offsetX, y: e.offsetY }};
                    }} else {{
                        drawFilledRectangle(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
                        startPoint = null;
                    }}
                }} else if (selectedMode === "drawOval") {{
                    if (!startPoint) {{
                        startPoint = {{ x: e.offsetX, y: e.offsetY }};
                    }} else {{
                        drawOval(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
                        startPoint = null;
                    }}
                }} else if (selectedMode === "drawFilledOval") {{
                    if (!startPoint) {{
                        startPoint = {{ x: e.offsetX, y: e.offsetY }};
                    }} else {{
                        drawFilledOval(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
                        startPoint = null;
                    }}
                }}
            }});

            function drawLine(ctx, x1, y1, x2, y2, color = "black") {{
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.strokeStyle = color;
                ctx.lineWidth = 2;
                ctx.stroke();
            }}

            function drawRectangle(ctx, x1, y1, x2, y2, color = "black") {{
                ctx.beginPath();
                ctx.strokeStyle = color;
                ctx.lineWidth = 2;
                ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
            }}

            function drawFilledRectangle(ctx, x1, y1, x2, y2, color = "black") {{
                ctx.fillStyle = color;
                ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
            }}

            function drawOval(ctx, x1, y1, x2, y2, color = "black") {{
                var radiusX = Math.abs(x2 - x1) / 2;
                var radiusY = Math.abs(y2 - y1) / 2;
                var centerX = Math.min(x1, x2) + radiusX;
                var centerY = Math.min(y1, y2) + radiusY;

                ctx.beginPath();
                ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
                ctx.strokeStyle = color;
                ctx.lineWidth = 2;
                ctx.stroke();
            }}

            function drawFilledOval(ctx, x1, y1, x2, y2, color = "black") {{
                var radiusX = Math.abs(x2 - x1) / 2;
                var radiusY = Math.abs(y2 - y1) / 2;
                var centerX = Math.min(x1, x2) + radiusX;
                var centerY = Math.min(y1, y2) + radiusY;

                ctx.beginPath();
                ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
                ctx.fillStyle = color;
                ctx.fill();
            }}
            socket.on('draw_line', function(data) {{
                eval(data.script); 
            }});
            socket.on('draw_oval', function(data) {{
                eval(data.script); 
            }});
            socket.on('draw_rectangle', function(data) {{
                eval(data.script); 
            }});
            socket.on('add_text', function(data) {{
                eval(data.script); 
            }});
            </script>
            """
        soup.body.append(BeautifulSoup(canvas_script, 'html.parser'))
        buttons_div.insert_after(script_tag)

    link_tag = soup.new_tag('link', rel='stylesheet', type='text/css', href='/static/styles.css')
    soup.head.append(link_tag)

    socket_script_tag = soup.new_tag('script',
                                     src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js",
                                     integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==",
                                     crossorigin="anonymous")
    soup.head.append(socket_script_tag)

    socket_script_tag = """
        <script type="text/javascript" charset="utf-8">
            var socket = io();
            socket.on('connect', function() {
                socket.emit('my event', {data: 'Im connected!'});
                console.log("socket connected");
            });

            function changeLabelText(labelId, newText) {
                var label = document.getElementById(labelId);
                if (label) {
                    label.innerText = newText;
                }
            }

            socket.on('change_label', function(data) {
                eval(data.script); // Execute the provided script
            });
        </script>
        """
    soup.head.append(BeautifulSoup(socket_script_tag, 'html.parser'))

    return str(soup)


def set_img_src_to_camera(img_src, camera_index=0):
    global video_feed_path
    video_feed_path = img_src

    if video_feed_path is not None:
        @app.route(video_feed_path)
        def video_feed():
            print("Waiting for camera response")
            return Response(generate_video_frame(camera_index), mimetype='multipart/x-mixed-replace; boundary=frame')


def generate_video_frame(camera_index):
    print("Generating camera feed")
    camera = cv2.VideoCapture(camera_index)
    while True:
        success, frame = camera.read()
        if not success:
            break
        else:
            ret, buffer = cv2.imencode('.jpg', frame)
            frame = buffer.tobytes()
            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

    camera.release()
    cv2.destroyAllWindows()


def add_css_file(css_path):
    global css_file_path
    css_file_path = css_path


def add_css(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')

    link_tag = soup.new_tag('link', rel='stylesheet', type='text/css', href=f'{css_file_path}')

    soup.head.append(link_tag)

    return str(soup)


@app.route('/')
def index():
    if file is not None:
        html_file_path = file
        html_content = read_html_file(html_file_path)
        if css_file_path is not None:
            modified_html_content = add_css(html_content)
        else:
            modified_html_content = html_content
        modified_html_content = add_event_listeners(modified_html_content)
        return modified_html_content
    else:
        return "File path not provided"


@app.route('/button_click', methods=['POST'])
def button_click():
    button_id = request.json['buttonId']
    button_click_signal.send(app, button_id=button_id)
    return "Button clicked"


@app.route('/button_hold', methods=['POST'])
def button_hold():
    button_id = request.json['buttonId']
    print(f"Button '{button_id}' is being held")
    button_hold_signal.send(app, button_id=button_id)
    return "Button hold event received"


@app.route('/text_field_content', methods=['POST'])
def text_field_content():
    data = request.json
    text_field_id = data['textFieldId']
    content = data['content']
    textfield_change_signal.send(app, text_field_id=text_field_id, content=content)
    print(f"Content of text field with ID '{text_field_id}': {content}")
    return "Text field content received"


@app.route('/image_click', methods=['POST'])
def image_click():
    data = request.json
    img_id = data['imgId']
    position = data['position']
    image_click_signal.send(app, img_id=img_id, position=position)
    return "Image click data received"


@socketio.on('change_label_text')
def change_label_text(label_id, new_text):
    script = f"changeLabelText('{label_id}', '{new_text}');"
    socketio.emit('change_label', {'script': script})


@socketio.on('draw_line')
def draw_line(canvas_id, x1, y1, x2, y2, color='black'):
    script = f"drawLine(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
    socketio.emit('draw_line', {'script': script})


@socketio.on('draw_oval')
def draw_oval(canvas_id, x1, y1, x2, y2, filled=False, color='black'):
    if filled:
        script = f"drawOval(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
    else:
        script = f"drawFilledOval(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
    socketio.emit('draw_oval', {'script': script})


@socketio.on('draw_rectangle')
def draw_rectangle(canvas_id, x1, y1, x2, y2, filled=False, color='black'):
    if filled:
        script = f"drawRectangle(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
    else:
        script = f"drawFilledRectangle(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
    socketio.emit('draw_rectangle', {'script': script})


@socketio.on('add_text')
def add_text(canvas_id, x, y, text, color='black', font='12px Arial'):
    script = f"""
    var canvas = document.getElementById("{canvas_id}");
    var ctx = canvas.getContext("2d");
    ctx.font = "{font}";
    ctx.fillStyle = "{color}";
    ctx.fillText("{text}", {x}, {y});
    """
    socketio.emit('add_text', {'script': script})