6
\$\begingroup\$

For a small project of mine, I will be needing to work a lot with braille translation. To do that, I have already finished a small program that I can use to translate braille into English letters. To make the whole thing a little more modular, I exported all my braille logic into a small module:

from typing import NoReturn, List
from bitmap import BitMap
from enum import Enum
from bidict import bidict


class Dot(Enum):
    TOP_LEFT = 0
    MIDDLE_LEFT = 1
    BOTTOM_LEFT = 2
    TOP_RIGHT = 3
    MIDDLE_RIGHT = 4
    BOTTOM_RIGHT = 5


class BrailleGlyph(BitMap):

    UNICODE_BLOCK = 10240
    MAPPING = bidict(
        {"00000000": " ",
         "00000001": "A",
         "00000011": "B",
         "00001001": "C",
         "00011001": "D",
         "00010001": "E",
         "00001011": "F",
         "00011011": "G",
         "00010011": "H",
         "00001010": "I",
         "00011010": "J",
         "00000101": "K",
         "00000111": "L",
         "00001101": "M",
         "00011101": "N",
         "00010101": "O",
         "00001111": "P",
         "00011111": "Q",
         "00010111": "R",
         "00001110": "S",
         "00011110": "T",
         "00100101": "U",
         "00100111": "V",
         "00111010": "W",
         "00101101": "X",
         "00111101": "Y",
         "00110101": "Z", }
    )

    def __init__(self, maxnum: int = 8) -> NoReturn:
        super().__init__(maxnum)
        self.history: List[Dot] = []

    def __str__(self) -> str:
        ones = int(self.tostring()[4:], 2)
        tens = int(self.tostring()[:4], 2) * 16
        return chr(BrailleGlyph.UNICODE_BLOCK + tens + ones)

    @classmethod
    def from_char(cls, char: str) -> "BrailleGlyph":
        return BrailleGlyph.fromstring(cls.MAPPING.inverse[char.upper()])

    def is_empty(self) -> bool:
        return self.history == []

    def click(self, index: Dot) -> NoReturn:
        if not self.test(index.value):
            self.set(index.value)
            self.history.append(index)

    def delete(self) -> NoReturn:
        self.flip(self.history.pop().value)

    def get_dots(self) -> List[Dot]:
        return self.history

    def to_ascii(self) -> str:
        try:
            return BrailleGlyph.MAPPING[self.tostring()]

        except KeyError:
            return "?"


class BrailleTranslator:

    def __init__(self) -> NoReturn:
        self.glyphs = [BrailleGlyph()]

    def __str__(self) -> str:
        return "".join(map(str, self.glyphs))

    def new_glyph(self) -> NoReturn:
        self.glyphs.append(BrailleGlyph())

    def delete(self) -> NoReturn:
        if self.glyphs[-1].is_empty():
            if len(self.glyphs) != 1:
                self.glyphs.pop()

        else:
            self.glyphs[-1].delete()

    def click(self, index: Dot) -> NoReturn:
        self.glyphs[-1].click(index)

    def get_current_glyph(self) -> BrailleGlyph:
        return self.glyphs[-1]

    def translate(self) -> str:
        return "".join(map(lambda x: x.to_ascii(), self.glyphs))

The primary use-case is going to be - me clicking dots (or some buttons on my keyboard that will set dots on the glyph) and the translation module will just remember what has been clicked and will display results in real-time.

To avoid reinventing too much I used the external bitmap and bidict libraries which both seem very fitting for a task like this.

Is there some way to make this more pythonic? Or maybe some obvious flaws?

Here is also a sample usage:

def braille_translator(self) -> NoReturn:
    graph = self.window["-GRAPH-"]
    output = self.window["-BRAILLE_OUTPUT-"]

    graphed_dots = []
    translator = BrailleTranslator()
    circle_mapping = {Dot.BOTTOM_LEFT: (50, 250),
                      Dot.BOTTOM_RIGHT: (150, 250),
                      Dot.MIDDLE_LEFT: (50, 150),
                      Dot.MIDDLE_RIGHT: (150, 150),
                      Dot.TOP_LEFT: (50, 50),
                      Dot.TOP_RIGHT: (150, 50), }
    dot_mapping = {"1": Dot.BOTTOM_LEFT,
                   "2": Dot.BOTTOM_RIGHT,
                   "4": Dot.MIDDLE_LEFT,
                   "5": Dot.MIDDLE_RIGHT,
                   "7": Dot.TOP_LEFT,
                   "8": Dot.TOP_RIGHT, }

    while True:
        event, values = self.window.read()
        if event in (None, 'Exit') or "EXIT" in event:
            exit()

        elif event in "124578":  # Left side of numpad!
            translator.click(dot_mapping[event])

        elif event == " ":
            translator.new_glyph()

        elif "BackSpace" in event:
            translator.delete()

        current_dots = translator.get_current_glyph().get_dots()
        for circle in [d for d in graphed_dots if d not in current_dots]:
            graph.delete_figure(circle)
            graphed_dots.remove(circle)

        for dot in [d for d in current_dots if d not in graphed_dots]:
            circle = graph.draw_circle(circle_mapping[dot],
                                       20, fill_color=GUI.THEME_COLOR)
            graphed_dots.append(circle)
        output.update(translator.translate())

And here's a GIF of the sample usage in action: Braille Translation

It's a thin line between over-fitting the translation module too much for my current task and keeping it modular for future uses!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ GUI.THEME_COLOR is undefined \$\endgroup\$ – Reinderien Apr 27 at 14:53
  • \$\begingroup\$ any reason why the bidict is 8-character long, when you only need 6? \$\endgroup\$ – hjpotter92 Apr 27 at 17:49
  • 1
    \$\begingroup\$ @Reinderien The sample usage isn't really part of what I want to be "reviewed", hence the GIF showing how that part of the code would work. To make the sample usage code runnable I would have to include the complete GUI code! \$\endgroup\$ – Mantas Kandratavičius Apr 28 at 8:51
  • \$\begingroup\$ @hjpotter92 That was the design decision of the developers of BitMap. Taking a look at how a BitMap gets initialized you can find - nbytes = (maxnum + 7) // 8 implying that 6 bit long bitmap wouldn't even be possible. \$\endgroup\$ – Mantas Kandratavičius Apr 28 at 8:52
1
\$\begingroup\$

First a few small things:

If you expect this to be used in other programs, it should be documented: comments to explain implementation choices and details and docstrings to explain how to use the classes and functions in the module.

A small suggestion: When modeling something, it is often helpful if the interface uses the same numbering or nomenclature used in the real world. For example, the upper left dot is often referred to as dot 1, not dot 0. So the class Dot(enum) might use values 1-6 rather than 0-5. But perhaps the names are the interface and the values are internal.

Multiplying by 16 is the same as shifting by 4 bits. That is, this:

def __str__(self) -> str:
    offset = int(self.tostring(), 2)
    return chr(BrailleGlyph.UNICODE_BLOCK + offset)

Is the same as

def __str__(self) -> str:
    ones = int(self.tostring()[4:], 2)
    tens = int(self.tostring()[:4], 2) * 16
    return chr(BrailleGlyph.UNICODE_BLOCK + tens + ones)

Use dict.get(key, default) with a default value instead of the try... except block:

def to_ascii(self) -> str:
    return BrailleGlyph.MAPPING.get(self.tostring(), "?")

The BitMap library is overkill and seems to get in the way. For example, the code converts the bits to a string, then converts the string to an int in base 2. But the bits were already stored as a byte in the bit map. I suspect it would be cleaner to implement the bit operations directly.

It seems odd that a BrailleGlyph object should keep a list of the dots in the order they were added (history). That's like the letter 'A' knowing what order the lines were added. .history is there so the editor can implement an 'undo' feature, but that seems to be a job for the editor, not the BrailleGlyph class.

\$\endgroup\$
1
  • \$\begingroup\$ you're right, history definitely shouldn't be part of that object! As for BitMap library, I definitely did consider implementing that part on my own, but assumed that there is definitely some extensive BitMap library around that I could use. In retrospect the library is far from extensive and doesn't even provide tools that I need the most. \$\endgroup\$ – Mantas Kandratavičius Apr 28 at 8:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.