How to Implement and Test a Java Whiteboard Transmission API

This article explains the design and testing of a virtual‑classroom whiteboard feature, covering the socket‑based transmission protocol, data encoding, message structures, coordinate system, supporting Java classes (Point, BoardBase, BoardUser), shape‑drawing utilities, and a runnable demo that generates base64‑encoded MessagePack payloads.

FunTester
FunTester
FunTester
How to Implement and Test a Java Whiteboard Transmission API

Interface Logic

User logs in and obtains a token.

Token and personal info are used to open a Socket connection.

The connection is registered with the same token and user info.

The client joins a specific room (roomId).

Whiteboard messages are sent over the Socket.

Data Source

Raw whiteboard data consists of point coordinates generated by a proprietary SDK. The SDK converts the points to binary, which is then Base64‑encoded into a short string (typically a few dozen characters).

Message Types

Start ("落笔") – includes a decimal color value derived from RGB.

Move ("移动")

End ("起笔")

Each message carries up to five coordinate points, a UUID‑style stroke ID, and a user ID (string). Start and End messages may contain fewer than five points.

Start and End messages can have fewer than five coordinate points.

Coordinate System

The whiteboard origin is at the top‑left corner (0,0). The bottom‑right corner is (1200,760). All points lie in the first quadrant.

Point Class

The Point class models a single point with scaled integer fields. It provides factory methods with bounds checking, an offset method, and a toString implementation.

package com.okayqa.board.base;

public class Point {
    public short x; // actual x = x / 10
    public short y; // actual y = y / 10
    public short width; // actual width = width / 100

    private Point(int x, int y, int width) {
        this.x = (short) (x * 10);
        this.y = (short) (y * 10);
        this.width = (short) (width * 100);
    }

    private Point(int x, int y) {
        this(x, y, 3);
    }

    public static Point getPoint(int x, int y) {
        if (x < 0 || x > 1199 || y < 0 || y > 760) return new Point(1, 1);
        return new Point(x, y);
    }

    public static Point getPoint(int x, int y, int width) {
        if (x < 0 || x > 1199 || y < 0 || y > 760) return new Point(1, 1, width);
        return new Point(x, y, width);
    }

    /**
     * Offset method
     * @param offsetX actual offset in x
     * @param offsetY actual offset in y
     */
    public Point move(int offsetX, int offsetY) {
        return getPoint(this.x / 10 + offsetX * 10, this.y / 10 + offsetY * 10);
    }

    @Override
    public String toString() {
        return "Point{ " + x + ", " + y + '}';
    }
}

BoardBase Utilities

BoardBase

extends the SDK‑provided MessageHandler and supplies static helpers for generating stroke IDs, random colors, and collections of points that represent geometric shapes (square, line, circle, heart). Data is packed with MessagePack and then Base64‑encoded.

package com.okayqa.board.base;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static com.okayqa.board.base.Point.getPoint;

public class BoardBase extends MessageHandler {
    private static final Logger logger = LoggerFactory.getLogger(BoardBase.class);

    public static List<Point> getFive(int x, int y) {
        if (x < 0 || x > 1199 || y < 0 || y > 760) return getFive(0, 0);
        List<Point> ps = new ArrayList<>();
        ps.add(getPoint(x, y));
        ps.add(getPoint(x + 1, y));
        ps.add(getPoint(x + 1, y + 1));
        ps.add(getPoint(x + 2, y + 1));
        ps.add(getPoint(x + 2, y + 2));
        return ps;
    }

    public static String getStrokeId() {
        String s = DEFAULT_STRING + UUID.randomUUID().toString();
        logger.info("Generated stroke ID: {}", s);
        return s;
    }

    public static int getColor() {
        return getRandomInt(0xFFFFFF);
    }

    public static List<Point> square(int cx, int cy, int halfWidth, int halfHeight) {
        List<Point> ps = new ArrayList<>();
        Point lt = getPoint(cx - halfWidth, cy - halfHeight);
        Point rt = getPoint(cx + halfWidth, cy - halfHeight);
        Point lb = getPoint(cx - halfWidth, cy + halfHeight);
        Point rb = getPoint(cx + halfWidth, cy + halfHeight);
        ps.addAll(line(lt, rt));
        ps.addAll(line(rt, rb));
        ps.addAll(line(rb, lb));
        ps.addAll(line(lb, lt));
        return ps;
    }

    public static List<Point> line(Point start, Point end) {
        List<Point> ps = new ArrayList<>();
        int xLen = end.x - start.x;
        int yLen = end.y - start.y;
        ps.add(start);
        for (int i = 1; i < xLen; i++) {
            int xx = start.x + i;
            int yy = start.y + (yLen / xLen) * i;
            ps.add(getPoint(xx, yy));
        }
        ps.add(end);
        return ps;
    }

    public static List<Point> circle(int cx, int cy, int r) {
        List<Point> ps = new ArrayList<>();
        double step = Math.PI / 60;
        for (int i = 0; i < 121; i++) {
            double xx = r * Math.cos(i * step) + cx;
            double yy = r * Math.sin(i * step) + cy;
            ps.add(getPoint((int) xx, (int) yy));
        }
        return ps;
    }

    public static List<Point> heart(int cx, int cy, int r) {
        List<Point> ps = new ArrayList<>();
        double step = Math.PI / 60;
        for (int i = 0; i < 121; i++) {
            double xx = r * (2 * Math.cos(i * step) - Math.cos(2 * i * step));
            double yy = r * (2 * Math.sin(i * step) - Math.sin(2 * i * step));
            ps.add(getPoint((int) yy + cx, (int) xx + cy));
        }
        return ps;
    }
}

BoardUser – Message Construction

BoardUser

builds on BoardBase to manage author information, board version, and a point collection. It provides high‑level methods for creating the three message types (start, move, end), clearing a page, and generating shape‑specific messages (square, circle, heart). The private write method packs the payload according to WriteType and returns a Base64‑encoded MessagePack string.

package com.okayqa.board.function;

import com.fun.utils.DecodeEncode;
import com.okayqa.board.base.BoardBase;
import com.okayqa.board.base.Document;
import com.okayqa.board.base.Point;
import com.okayqa.board.base.WriteType;
import org.msgpack.core.MessagePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class BoardUser extends BoardBase {
    private static final Logger logger = LoggerFactory.getLogger(BoardUser.class);
    private String author;
    private byte version = Document.BOARD_VERSION_INITIAL;
    private List<Point> ps;

    BoardUser(String author) { this.author = author; }

    public String writeStart(List<Point> ps, String strokeId, byte page) {
        return write(ps, strokeId, page, WriteType.START);
    }

    public String writeMove(List<Point> ps, String strokeId, byte page) {
        return write(ps, strokeId, page, WriteType.MOVE);
    }

    public String writeEnd(List<Point> ps, String strokeId, byte page) {
        return write(ps, strokeId, page, WriteType.END);
    }

    public String clearPage(byte page) {
        var packer = MessagePack.newDefaultBufferPacker();
        writeClearPage(Document.CURRENT_BOARD_VERSION, author, page, packer);
        return DecodeEncode.base64Encode(packer.toByteArray());
    }

    private String write(List<Point> ps, String strokeId, byte page, WriteType type) {
        var packer = MessagePack.newDefaultBufferPacker();
        switch (type) {
            case START:
                writeStartWrite(version, author, page, strokeId, getColor(), ps, packer);
                break;
            case MOVE:
                writeContinueWrite(version, author, page, strokeId, ps, packer);
                break;
            case END:
                writeEndWrite(version, author, page, strokeId, ps, packer);
                break;
        }
        return DecodeEncode.base64Encode(packer.toByteArray());
    }

    public List<String> initGraph(String strokeId, byte page) {
        List<String> msgs = new ArrayList<>();
        if (ps == null || ps.isEmpty()) return msgs;
        for (int i = 0; i < ps.size(); i += 5) {
            List<Point> segment = ps.subList(i, Math.min(i + 5, ps.size()));
            if (msgs.isEmpty()) {
                msgs.add(writeStart(segment, strokeId, page));
            } else if (i + 5 >= ps.size()) {
                msgs.add(writeEnd(segment, strokeId, page));
            } else {
                msgs.add(writeMove(segment, strokeId, page));
            }
        }
        return msgs;
    }

    public List<String> writeSquare() {
        ps = square(500, 300, 200, 100);
        return initGraph(getStrokeId(), (byte) 0);
    }

    public List<String> writeCircle() {
        ps = circle(500, 300, 250);
        return initGraph(getStrokeId(), (byte) 0);
    }

    public List<String> writeHeart() {
        ps = heart(500, 300, 200);
        return initGraph(getStrokeId(), (byte) 0);
    }

    public List<String> move(int offsetX, int offsetY) {
        ps = ps.stream().map(p -> p.move(offsetX, offsetY)).collect(Collectors.toList());
        return initGraph(getStrokeId(), (byte) 0);
    }
}

Demo Execution

public static void main(String[] args) throws IOException {
    BoardUser boardUser = new BoardUser(Users.getTeaUser(0));
    List<String> msgs = boardUser.writeCircle();
    for (String msg : msgs) {
        System.out.println(msg);
    }
}

The console logs show the current user, the generated stroke ID, and a series of Base64‑encoded MessagePack strings that represent the whiteboard drawing data.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaGraphicsWebSocketMessagePackAPI testingWhiteboard
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.