Skip to content

Commit

Permalink
Multi-line direct support, input handling and error solving (#2)
Browse files Browse the repository at this point in the history
* Rename module

* Update requirements.txt

* Remove debugging statement

* Solve repeated coordinate issue

Solved an issue that caused spline generation to fail when two consecutive equal coordinates were drawn.

* Update warning message

* Add comments

* Add input handling method

Added a method that allows extensive input handling, including input from the command line interface and adding standard Python input as fallback method.

* Relocate modules

* Distribute configuration constants

* Prevent spline generation error on empty line

* Add connection-related methods

Added methods that allow connection and disconnection from the `matplotlib` figure. Also added a status check.

* Add support for line switching

Added line switching functionality via numeric keyboard input. This prevents the requirement to close and reopen the plot on every new line, as well as the single-line-editing restriction.

* Set constant axis

* Distribute calback functions

* Rename variables and organize code

* Update title styles

* Fix imports

* Update line index handling and display

Updated the whole line index handling process since displaying line indices from 1 would be more intuitive than displaying them from 0.
  • Loading branch information
erlete authored Dec 25, 2022
1 parent 23fd7a4 commit 4f6564c
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 117 deletions.
164 changes: 74 additions & 90 deletions src/coordinate-canvas/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,123 +15,107 @@


import json
import sys
from itertools import cycle

import matplotlib.pyplot as plt
from bidimensional import Coordinate
from bidimensional.functions import Spline

from .config import CONFIG
from .line_builder import LineBuilder
from .core.config import COLORS, POSITIONS
from .core import input_handler as ih
from .core.line_builder import LineBuilder


def validate_input(message: str) -> float:
"""Input validation method.
# Constants' definition:

This method validates a given input. If the input is not numeric, then a
ValueError is raised.
COLORS = cycle(COLORS)
AX = plt.gca()
FIG = plt.gcf()

Args:
message (str): The message to be displayed to the user.

Raises:
ValueError: If the input is not numeric.
# Methods' definition:

Returns:
float: The validated input.
"""
def decide(event):

value = input(message)
if event.key.isnumeric() and 1 <= int(event.key) <= LINE_COUNT:
lines[current_data[1]].get("line_builder").disconnect()
lines[int(event.key) - 1].get("line_builder").connect()

if not value.isnumeric():
raise ValueError("the input value must be numeric")
current_data[0] = lines[int(event.key) - 1].get("line")
current_data[1] = int(event.key) - 1

return float(value)
FIG.suptitle(
f"Click to add points for line number {current_data[1] + 1}...",
fontsize="large", fontweight="bold"
)


# Constants' definition:
def close(event):
if event.key == "escape":
exit(0)

COLORS = cycle(CONFIG.get("colors"))

# Parameter input:

width = validate_input("Enter width: ")
height = validate_input("Enter height: ")
line_count = int(validate_input("Enter the number of lines to draw: "))
input_data = ih.cli_input(sys.argv)

if input_data is None:
input_data = (
ih.python_input("Width: "),
ih.python_input("Height: "),
ih.python_input("Number of lines to draw: ")
)

WIDTH, HEIGHT, LINE_COUNT = ih.output_format(input_data)

# Constants' configuration:

FIG.canvas.mpl_connect("key_press_event", decide)
FIG.canvas.mpl_connect("key_release_event", close)

plt.grid(True)
AX.set_xlim(0, WIDTH)
AX.set_ylim(0, HEIGHT)

# Data output template:

data = {
f"line_{index + 1}": {
f"line_{index}": {
"x": [],
"y": []
} for index in range(line_count)
} for index in range(1, LINE_COUNT + 1)
}

# Main loop:

color_cache = []
for index in range(line_count):
color_cache.append(next(COLORS))

# Figure setup:

fig, ax = plt.subplots()
plt.grid(True)
ax.set_xlim(0, width)
ax.set_ylim(0, height)
ax.set_title(f"Click to add points for line number {index + 1}...")

# Previous drawings' plotting:

if index > 0:
for i in range(index):
sub_color = color_cache[i]

coordinates = [
Coordinate(x_, y_)
for x_, y_ in zip(
data[f"line_{i + 1}"]['x'],
data[f"line_{i + 1}"]['y']
)
]

sp = Spline(
coordinates,
gen_step=min(width, height) / 1000
)

sp.plot_input(
CONFIG.get("input").get("shape"),
ms=CONFIG.get("input").get("size"),
alpha=CONFIG.get("input").get("alpha"),
color=f"dark{sub_color}",
)

sp.plot_positions(
CONFIG.get("positions").get("shape"),
lw=CONFIG.get("positions").get("size"),
alpha=CONFIG.get("positions").get("alpha"),
color=sub_color
)

# Line drawing and display:

line, = ax.plot(
[], [],
CONFIG.get("positions").get("shape"),
lw=CONFIG.get("positions").get("size") * 2, # Highlights the line.
alpha=CONFIG.get("positions").get("alpha"),
color=color_cache[-1]
)
builder = LineBuilder(line, ax, width, height, color_cache[-1])

plt.show()

# Data storage:

data[f"line_{index + 1}"]['x'].extend(builder.x)
data[f"line_{index + 1}"]['y'].extend(builder.y)
lines = [
{
"color": (color := next(COLORS)),
"line": (line := AX.plot(
[], [],
POSITIONS.get("shape"),
lw=POSITIONS.get("size"),
alpha=POSITIONS.get("alpha"),
color=color
)[0]),
"line_builder": LineBuilder(line, AX, WIDTH, HEIGHT, color)
}
for _ in range(LINE_COUNT)
]

# Initial connection and setting:

current_data = [lines[0].get("line"), 0]
lines[0].get("line_builder").connect()
FIG.suptitle("Click to add points for line number 1...",
fontsize="large", fontweight="bold")
AX.set_title(f"Press keys 1 - {LINE_COUNT} to change lines or ESC to exit",
fontsize="medium", fontstyle="italic")

plt.show()

# Data storage:

for index in range(LINE_COUNT):
data[f"line_{index + 1}"]['x'].extend(lines[index].get("line_builder").x)
data[f"line_{index + 1}"]['y'].extend(lines[index].get("line_builder").y)

# Data output:

Expand Down
16 changes: 0 additions & 16 deletions src/coordinate-canvas/config.py

This file was deleted.

16 changes: 16 additions & 0 deletions src/coordinate-canvas/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
COLORS = [
"red", "green", "salmon", "blue",
"orange", "violet", "goldenrod", "gray"
]

INPUT = {
"shape": "o",
"size": 5,
"alpha": 1
}

POSITIONS = {
"shape": "-",
"size": 1,
"alpha": 0.5
}
96 changes: 96 additions & 0 deletions src/coordinate-canvas/core/input_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Input handling module.
This module contains all methods needed to handle the input of the package.
It is mean to be used internally by the package and not by the user, since
method implementations are highly specific to the package's needs.
Author:
Paulo Sanchez (@erlete)
"""


import re


def output_format(values: tuple[str]) -> tuple[float, float, int] | None:
"""Formats the output of the input.
This method receives a tuple of strings and validates them. If the
amount of values is less than 3, then a ValueError is raised. If any
of the values is not a string, then a TypeError is raised. Otherwise,
the formatted input values are returned.
The method is meant to be used in combination with the `cli_input`
and the `python_input` methods.
Args:
values (tuple): The input values.
Raises:
ValueError: If the input does not have at least 3 values.
TypeError: If any of the input values is not a string.
Returns:
tuple[float, float, int]: The formatted input values.
"""

if len(values) < 3:
raise ValueError("the input must have at least 3 values")

if any(not isinstance(value, str) for value in values):
raise TypeError("the input values must be strings")

return (float(values[0]), float(values[1]), int(float(values[2])))


def cli_input(arguments: list[str]) -> tuple[str, str, str] | None:
"""Handles CLI input.
This class receives a list of arguments and validates them. If the
amount of arguments is less than 4, then None is returned. If any of
the arguments is not numeric, then None is returned. Otherwise, the
raw input values are returned.
Args:
arguments (list[str]): The list of arguments from `sys.argv`.
Returns:
tuple[str, str, str] | None: The raw input values or None.
"""

if len(arguments) < 4:
return None

values = arguments[1:4]

if any(not (bool(re.match(r"\d+\.\d+", value))
or value.isnumeric()) for value in values):

return None

return values


def python_input(message: str) -> str:
"""Handles Python input.
This class receives a message and validates the input. If the input
is not numeric, then a ValueError is raised. Otherwise, the input is
returned.
Args:
message (str): The message to be displayed to the user.
Raises:
ValueError: If the input is not numeric.
Returns:
str: The validated input or None.
"""

value = input(message)

if not (bool(re.match(r"\d+\.\d+", value)) or value.isnumeric()):
raise ValueError("the input must be numeric")

return value
Loading

0 comments on commit 4f6564c

Please sign in to comment.