2
\$\begingroup\$

I want to know how to improve the design of Canvas drawing implementation.

Canvas Drawing

This solution is to implement a console-based canvas drawing application.

Problem Statement

Description

You're given the task of writing a simple console version of a drawing program. At this time, the functionality of the program is quite limited but this might change in the future. In a nutshell, the program should work as follows:

  1. Create a new canvas
  2. Start drawing on the canvas by issuing various commands
  3. Quit

Commands

  • C w h Should create a new canvas of width w and height h.
  • L x1 y1 x2 y2 Should create a new line from (x1,y1) to (x2,y2). Currently only horizontal or vertical lines are supported. Horizontal and vertical lines will be drawn using the 'x' character.
  • R x1 y1 x2 y2 Should create a new rectangle, whose upper left corner is (x1,y1) and lower right corner is (x2,y2). Horizontal and vertical lines will be drawn using the 'x' character.
  • B x y c Should fill the entire area connected to (x,y) with "color" c. The the behavior of this is the same as that of the "bucket fill" tool in paint programs.
  • Q Should quit the program.

Sample I/O

Below is a sample run of the program. User input is prefixed with entering the command:

enter the command: C 20 4
----------------------
|                    |
|                    |
|                    |
|                    |
----------------------

enter command: L 1 2 6 2
----------------------
|                    |
|xxxxxx              |
|                    |
|                    |
----------------------

enter command: L 6 3 6 4
----------------------
|                    |
|xxxxxx              |
|     x              |
|     x              |
----------------------

enter command: R 14 1 18 3
----------------------
|             xxxxx  |
|xxxxxx       x   x  |
|     x       xxxxx  |
|     x              |
----------------------

enter the command: B 10 3 o
----------------------
|oooooooooooooxxxxxoo|
|xxxxxxooooooox   xoo|
|     xoooooooxxxxxoo|
|     xoooooooooooooo|
----------------------

enter the command: Q

Design

Class Overview

  • org.draw.paint
    • CanvasRunner - Entry Point.It reads the console command and executes them.
  • org.draw.paint.painter
    • Painter - Base for all painters
    • BoxPainter - Painter to draw a Box in the canvas.
    • FillPainter - Painter to Fill closed area in the canvas with the given color.
    • LinePainter - Painter to draw a line in the canvas
    • RectanglePainter - Painter to draw a rectangle on the canvas.
    • CanvasPainter - Draws the Canvas with giving width and height.
  • org.draw.paint.canvas
    • ICanvas - Base for all Canvas
    • Canvas - Area with the given width and Height that holds drawing
    • Pixel - Tiny part of Canvas which hold the draw value.
    • CanvasConstants - Canvas Constants.

Class Diagram Overview


Class Diagram

Code

Canvas Runner

package org.draw.paint

import org.draw.paint.canvas.Canvas
import org.draw.paint.canvas.ICanvas
import org.draw.paint.command.CommandParser
import org.draw.paint.common.Status
import org.draw.paint.exception.CanvasExceptions
import org.slf4j.LoggerFactory
import java.util.*

fun main(args: Array<String>) {
    val canvasRunner  = CanvasRunner()
    canvasRunner.start()
}

class CanvasRunner {

    companion object {
        private val log =  LoggerFactory.getLogger(CanvasRunner::class.java)
    }

    private var canvas: ICanvas =  Canvas()

    private fun readLine(): String {
        val scanner = Scanner(System.`in`)
        return scanner.nextLine()
    }

    internal fun executeCommand(command: String) {


        val status = CommandParser.parse(cmd = command).paint(canvas = canvas)

        if(status.isSuccess()) {
            canvas.printScreen()
        }

        Status.statusProcessor(status = status)
    }

    private fun executeConsoleCommands() {
        var command: String

        do {
            print("Enter Command :")
            command = this.readLine()
            log.trace("Received instruction as $command")
            this.executeCommand(command = command)

        } while (true)
    }


    fun start() {
        executeConsoleCommands()
    }

}

Command Parser

package org.draw.paint.command

import org.draw.paint.canvas.ICanvas
import org.draw.paint.canvas.Position
import org.draw.paint.common.Failed
import org.draw.paint.common.Status
import org.draw.paint.exception.CanvasExceptions
import org.draw.paint.painter.*
import org.slf4j.LoggerFactory
import kotlin.system.exitProcess

object CommandParser  {

    private val createPainterFunList =  mutableListOf<(String) -> Painter?> (
        this::createCanvasPainter,
        this::createFillCommandPainter,
        this::createLineCommandPainter,
        this::quiteCommandPainter,
        this::rectangleCommandPainter
    )

    private val log =  LoggerFactory.getLogger(CommandParser::class.java)

    fun parse(cmd: String): Painter {

        var failed: Painter =   object: Painter {
            override fun validate(canvas: ICanvas): Status = Failed(reason = "Unable to Execute Command")
            override fun paint(canvas: ICanvas) : Status = Failed(reason = "Unable to Execute Command")
        }

        try {

            this.createPainterFunList.forEach { fn ->
                val painter =  fn(cmd)
                if(painter !=  null) return painter
            }

        } catch (e: Exception) {

            failed =   object: Painter {
                override fun validate(canvas: ICanvas): Status = Failed(reason = e.localizedMessage)
                override fun paint(canvas: ICanvas) : Status = Failed(reason = e.localizedMessage)
            }
        }

        return failed
    }

    private fun parseCommand(regex: Regex, string: String): MatchResult.Destructured ? {

        try {

            if(regex.matches(string)) {
                val result  =  regex.find(string)
                return result?.destructured ?: throw CanvasExceptions("Unable to parse Canvas Command")
            }

        } catch (e: Exception) {
            log.error("Error Parsing the Canvas Command", e)
            throw CanvasExceptions(throwable = e)
        }

        return  null
    }


    fun createCanvasPainter(cmd: String):Painter? {

        val cmdPattern = """[Cc]\s+(\d+)\s+(\d+)""".toRegex()

        val  (width , height) = this.parseCommand(regex = cmdPattern, string = cmd) ?: return null
        return  CanvasPainter(width = width.toInt(), height = height.toInt())

    }


    private fun createFillCommandPainter(cmd: String): Painter? {

        val templateRegex =  """[Bb]\s+(\d+)\s+(\d+)\s+(.)""".toRegex()

        val (sw,sh,colour) =  this.parseCommand(regex = templateRegex, string = cmd) ?: return null
        return  FillPainter(start = Position(x = sw.toInt(), y = sh.toInt()) ,colour = colour)

    }

    private fun createLineCommandPainter(cmd:String): Painter? {

        val templateRegex =  """[Ll]\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)""".toRegex()

        val (sw,sh,ew,eh) = this.parseCommand(regex = templateRegex, string = cmd) ?: return null

        return  LinePainter(start = Position(x = sw.toInt(), y= sh.toInt()),
            end = Position(x = ew.toInt(), y = eh.toInt()))
    }

    private fun quiteCommandPainter(cmd: String) : Painter? {

        val templateRegex =  """[Qq]""".toRegex()

        if(templateRegex.matches(cmd)) {
            exitProcess(status = 0)
        }

        return null
    }


    private  fun rectangleCommandPainter(cmd: String): Painter? {

        val templateRegex =  """[Rr]\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)""".toRegex()
        val (sw,sh,ew,eh) = this.parseCommand(regex = templateRegex, string = cmd) ?: return null

        return  RectanglePainter(start = Position(x = sw.toInt(), y = sh.toInt()),
            end = Position(x = ew.toInt(), y = eh.toInt()))

    }

}

Status Enum

package org.draw.paint.common

enum class StatusTypes {
    SUCCESS, FAILED
}



open class Status(val status: StatusTypes) {

    companion object{
        fun statusProcessor(status: Status) {
            when(status) {
                is Success -> println("Done.")
                is Failed -> println("Err: ${status.reason}")
            }
        }
    }

    fun isSuccess(): Boolean {
        return status == StatusTypes.SUCCESS
    }
}

class Success: Status(status = StatusTypes.SUCCESS)

class Failed(val reason: String):Status(status = StatusTypes.FAILED)

Fill Painter

package org.draw.paint.painter

import org.draw.paint.canvas.ICanvas
import org.draw.paint.canvas.Position
import org.draw.paint.common.Failed
import org.draw.paint.common.Status
import org.draw.paint.common.Success
import org.slf4j.LoggerFactory

class FillPainter(private val start:Position, private val colour: String): Painter {

    val affectedPositions = mutableListOf<Position>()

    companion object {
        private val log = LoggerFactory.getLogger(FillPainter::class.java)
    }

    override fun validate(canvas: ICanvas): Status {

        return if( !canvas.isPositionAvailable(start) ) {
            Failed("Position Not available")
        } else if(!canvas.isPositionWritable(start)){
            Failed("Position Filled already")
        } else {
            Success()
        }
    }


    override fun paint(canvas: ICanvas): Status {
        var status = this.validate(canvas = canvas)

        if(status is Failed) {
            return status
        }

        status = this.fillByBreadthFirstSearch(canvas = canvas)

        log.debug("Affected positions Count =  ${affectedPositions.size}")
        log.trace("Affected positions : $affectedPositions")
        log.info("Painting of LinePainter completed ")
        return status
    }


    private fun fillByBreadthFirstSearch(canvas: ICanvas) : Success {

        var temp = start
        val trackList =  mutableListOf<Position>()
        canvas.setPixelValueAt(pos = temp, value = colour, overwrite = false)
        affectedPositions.add(temp)

        var children =  canvas.writableChildrenOf(pos = temp)

        children.forEach { p ->
            canvas.setPixelValueAt(pos = p, value = colour, overwrite = false)
            affectedPositions.add(p)
            trackList.add(p)
        }

        while(trackList.size > 0) {
            temp =  trackList.last()
            trackList.removeAt(trackList.size - 1)

            children =  canvas.writableChildrenOf(pos = temp)

            children.forEach { p ->
                canvas.setPixelValueAt(pos = p, value = colour, overwrite = false)
                affectedPositions.add(p)
                trackList.add(p)
            }
        }

        return Success()
    }
}

Canvas

package org.draw.paint.canvas

import org.slf4j.LoggerFactory

class Canvas(private var width: Int = 0, private var height: Int = 0) : ICanvas {

    companion object {
        private val logger =  LoggerFactory.getLogger(Canvas::class.java)
    }

    private var pixels: Array<Array<Pixel>>

    init {

        pixels = initPixel(width = width, height = height)
    }

    private fun initPixel(width: Int, height: Int): Array<Array<Pixel>> {

        return Array(size = height + 2) {i ->
            Array(size = width + 2){j ->
                if(i == 0 || i == height + 1) {
                    WidthBorder()
                } else if(j == 0 || j == width + 1) {
                    HeightBorder()
                } else {
                    Pixel(txt = CanvasConstants.DEFAULT_DISPLAY_CHAR)
                }
            }
        }
    }

    override fun createCanvas(width: Int, height: Int): Boolean {

        return if(this.width == 0  || this.height == 0) {

            this.width =  width
            this.height =  height
            this.pixels = initPixel(width = width, height = height)
            true

        } else {

            false
        }


    }

    override fun printScreen() {
        for(i in this.pixels.indices) {
            if(i != 0) {
                println("")
            }
            for(j in this.pixels[i].indices) {
                print(this.pixels[i][j])
            }
        }
        println()
    }

    override fun isPositionAvailable(pos: Position): Boolean {

        if(this.pixels.isEmpty()) {
            logger.error("Position is not available")
            return false
        }

        return pos.x > 0
                && pos.y >0
                && this.pixels.size -1  > pos.y
                && this.pixels[0].size -1 > pos.x
    }

    override fun isPositionWritable(pos: Position): Boolean {

        return this.isPositionAvailable(pos)
                && this.pixels[pos.y][pos.x].isBlank()
    }

    override fun getPixelValueAt(pos: Position): String {

        return if(this.isPositionAvailable(pos)) {
                this.pixels[pos.y][pos.x].text
            } else {
                CanvasConstants.INVALID_TEXT_CHAR
            }
    }

    override fun setPixelValueAt(pos: Position, value: String, overwrite: Boolean) : Boolean {
        return  if(!overwrite && this.isPositionWritable(pos = pos)) {
                this.pixels[pos.y][pos.x].text =  value
                true
            }  else if(overwrite){
                this.pixels[pos.y][pos.x].text =  value
                true
            } else {
                false
            }
    }

    override fun setPixelValueBetween(inclusiveStart: Position,
                                      inclusiveEnd: Position,
                                      value: String,
                                      overwrite: Boolean) : List<Position> {

        val start =  inclusiveStart
        val end =  inclusiveEnd
        val affectedList =  mutableListOf<Position>()

        if(!this.isPositionAvailable(pos = start)
            || !this.isPositionAvailable(pos = end)) {
            return  affectedList
        }

        val yRange = if (start.y <= end.y) start.y..end.y else start.y downTo end.y
        val xRange = if (start.x <= end.x) start.x..end.x else start.x downTo  end.x

        for(y in yRange) {
            for(x in xRange) {
                val pixel = this.pixels[y][x]
                pixel.text = value
                affectedList.add(Position(x = x, y = y))
            }
        }

        return affectedList
    }

    override fun writableChildrenOf(pos: Position): List<Position> {
        val list =  mutableListOf<Position>()

        if (this.isPositionWritable(Position(x = pos.x, y = pos.y + 1 ))) {
            list.add(Position(x = pos.x, y = pos.y + 1 ))
        }

        if (this.isPositionWritable(Position(x = pos.x, y = pos.y - 1 ))) {
            list.add(Position(x = pos.x, y = pos.y - 1 ))
        }

        if (this.isPositionWritable(Position(x = pos.x + 1, y = pos.y))) {
            list.add(Position(x = pos.x + 1, y = pos.y))
        }

        if (this.isPositionWritable(Position(x = pos.x - 1, y = pos.y))) {
            list.add(Position(x = pos.x - 1, y = pos.y))
        }

        return list
    }
}

Canvas Interface

package org.draw.paint.canvas

interface ICanvas {

    fun createCanvas(width: Int, height:Int): Boolean
    fun printScreen()
    fun isPositionAvailable(pos: Position): Boolean
    fun isPositionWritable(pos: Position): Boolean
    fun getPixelValueAt(pos: Position): String
    fun setPixelValueAt(pos: Position, value: String, overwrite: Boolean = true) : Boolean
    fun setPixelValueBetween(inclusiveStart: Position,
                             inclusiveEnd: Position,
                             value: String,
                             overwrite: Boolean = true) : List<Position>
    fun writableChildrenOf(pos: Position): List<Position>
}

Pixel

package org.draw.paint.canvas

open class Pixel() {

    var text: String = ""

    constructor(txt: String): this() {
        this.text = txt
    }

    fun isBlank():Boolean {
        return this.text.isBlank()
    }


    override fun equals(other: Any?): Boolean {
        return this.text == (other as? Pixel)?.text ?: "UNKNOWN"
    }

    override fun hashCode(): Int {
        return this.text.hashCode()
    }

    override fun toString(): String {
        return this.text
    }
}

data class Position(val x: Int, val y:Int)

class WidthBorder : Pixel( txt = CanvasConstants.WIDTH_BORDER_CHAR)

class HeightBorder : Pixel(txt =  CanvasConstants.HEIGHT_BORDER_CHAR)
\$\endgroup\$
  • \$\begingroup\$ Pasted code as suggested. I want to know is there a way to improve the design. I have tried so many time but I could not refactor beyond this. Wanted to know if can be further improved in terms of design \$\endgroup\$ – Ramanathan Ganesan Jul 7 at 15:48

Your Answer

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

Browse other questions tagged or ask your own question.