package org.openpnp.gui.importer.rs274x;
import java.awt.geom.Point2D;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openpnp.model.BoardPad;
import org.openpnp.model.LengthUnit;
import org.openpnp.model.Location;
import org.openpnp.model.Pad;
import org.pmw.tinylog.Logger;
/**
* A simple RS-274X parser. Not intended to be a general parser, but implements only OpenPnP
* specific functionality.
*/
public class Rs274xParser {
enum LevelPolarity {
Dark, Clear
}
enum InterpolationMode {
Linear, Clockwise, CounterClockwise
}
private BufferedReader reader;
// Context
private LengthUnit unit;
private Aperture currentAperture;
private Point2D.Double currentPoint;
private LevelPolarity levelPolarity;
private InterpolationMode interpolationMode;
private boolean multiQuadrantMode;
private boolean regionMode;
private int coordinateFormatIntegerLength;
private int coordinateFormatDecimalLength;
private boolean coordinateFormatTrailingZeroOmission;
private boolean coordinateFormatIncremental;
private Map<Integer, Aperture> apertures = new HashMap<>();
/**
* Maps Aperture indexes to a count to aid in generation of pad names.
*/
private Map<Integer, Integer> apertureUseCounts = new HashMap<>();
private boolean stopped;
private int lineNumber;
private ParseStatistics parseStatistics;
private boolean regionStarted;
private List<BoardPad> pads;
public Rs274xParser() {
reset();
}
/**
* Parse the given File for solder paste pads.
*
* @see #parseSolderPastePads(Reader)
* @param file
* @return
* @throws Exception
*/
public List<BoardPad> parseSolderPastePads(File file) throws Exception {
Logger.info("Parsing " + file);
return parseSolderPastePads(new FileReader(file));
}
/**
* Parse the input from the Reader extracting individual pads to be used for solder paste
* application. It is expected that the input is is an RS-274X Gerber solder paste layer.
*
* Currently this code only parses out single flashes of rectangular, circular and oblong
* apertures. Ideas for future versions include rendering the entire file and uses blob
* detection and contour finding to create polygon pads.
*
* Another option is to consider each operation it's own element/shape. This is how gerbv seems
* to do it.
*
* @param reader
* @return
* @throws Exception
*/
public List<BoardPad> parseSolderPastePads(Reader reader) throws Exception {
reset();
this.reader = new BufferedReader(reader);
try {
while (!stopped) {
readCommand();
}
}
catch (Exception e) {
parseStatistics.errored = true;
error("Uncaught error: " + e.getMessage());
}
return pads;
}
private void readCommand() throws Exception {
if (peek() == '%') {
readExtendedCodeCommand();
}
else {
readFunctionCodeCommand();
}
}
private void readFunctionCodeCommand() throws Exception {
// a command is either a D, G, M or coordinate data
// followed by a D.
// X, Y
// TODO: Make sure this becomes the current point.
Point2D.Double coordinate = new Point2D.Double(currentPoint.getX(), currentPoint.getY());
// I, J
Point2D.Double arcCoordinate = new Point2D.Double(0, 0);
while (!stopped) {
int ch = read();
switch (ch) {
case '*': {
// Empty block or end of block that was not terminated
// from a previous read.
return;
}
case 'D': {
readDcode(coordinate, arcCoordinate);
return;
}
case 'G': {
readGcode();
return;
}
case 'M': {
readMcode();
return;
}
// TODO: See 7.2 Coordinate Data without Operation Code
case 'X': {
coordinate.x = readCoordinateValue();
break;
}
case 'Y': {
coordinate.y = readCoordinateValue();
break;
}
case 'I': {
arcCoordinate.x = readCoordinateValue();
break;
}
case 'J': {
arcCoordinate.y = readCoordinateValue();
break;
}
default: {
error("Unknown function code " + ((char) ch));
}
}
}
}
// G54D06
private void readGcode() throws Exception {
int code = readInteger();
switch (code) {
case 1: {
interpolationMode = InterpolationMode.Linear;
break;
}
case 2: {
interpolationMode = InterpolationMode.Clockwise;
break;
}
case 3: {
interpolationMode = InterpolationMode.CounterClockwise;
break;
}
case 4: {
// comment, ignore
readUntil('*');
break;
}
case 36: {
enableRegionMode();
break;
}
case 37: {
disableRegionMode();
break;
}
case 54: {
// deprecated, prepare to select aperture
break;
}
case 55: {
// deprecated, prepare for flash
break;
}
case 70: {
// deprecated, unit inch
unit = LengthUnit.Inches;
break;
}
case 71: {
// deprecated, unit mm
unit = LengthUnit.Millimeters;
break;
}
case 74: {
multiQuadrantMode = false;
break;
}
case 75: {
multiQuadrantMode = true;
break;
}
case 90: {
// deprecated, absolute coordinate mode
coordinateFormatIncremental = false;
break;
}
case 91: {
// deprecated, incremental coordinate mode
coordinateFormatIncremental = true;
break;
}
default: {
warn("Unknown G code " + code);
}
}
}
private void readDcode(Point2D.Double coordinate, Point2D.Double arcCoordinate)
throws Exception {
int code = readInteger();
switch (code) {
case 1: {
performD01(coordinate, arcCoordinate);
break;
}
case 2: {
performD02(coordinate);
break;
}
case 3: {
performD03(coordinate);
break;
}
default: {
if (code < 10) {
error("Unknown reserved D code " + code);
}
// anything else is an aperture code, so look up the aperture
// and set it as the current
currentAperture = apertures.get(code);
if (currentAperture == null) {
warn("Unknown aperture " + code);
}
}
}
}
private void readMcode() throws Exception {
int code = readInteger();
switch (code) {
case 0: {
// deprecated version of stop command
stopped = true;
break;
}
case 2: {
stopped = true;
break;
}
case 1: {
// deprecated, does nothing
break;
}
default: {
warn("Unknown M code " + code);
}
}
}
/**
* Linear or circular interpolation. If in region mode, add a line or arc to the current
* contour. Otherwise draw a line or arc.
*
* @param coordinate
* @param arcCoordinate
* @throws Exception
*/
private void performD01(Point2D.Double coordinate, Point2D.Double arcCoordinate)
throws Exception {
if (interpolationMode == null) {
error("Interpolation most must be set before using D02");
}
if (regionMode) {
if (interpolationMode == InterpolationMode.Linear) {
addRegionLine(coordinate);
}
else {
addRegionArc(coordinate, arcCoordinate);
}
}
else {
if (interpolationMode == InterpolationMode.Linear) {
parseStatistics.lineCount++;
warn("Linear interpolation not yet supported");
}
else {
parseStatistics.arcCount++;
warn("Circular interpolation not yet supported");
}
}
currentPoint = coordinate;
}
/**
* Move / set the current coordinate. Additionally, in region mode end the current contour.
*
* @param coordinate
* @throws Exception
*/
private void performD02(Point2D.Double coordinate) throws Exception {
if (interpolationMode == null) {
error("Interpolation mode must be set before using D02");
}
if (regionMode) {
closeRegion();
}
currentPoint = coordinate;
}
/**
* Flash the current aperture at the given coordinate.
*
* @param coordinate
* @throws Exception
*/
private void performD03(Point2D.Double coordinate) throws Exception {
if (currentAperture == null) {
error("Can't flash, no current aperture");
}
if (regionMode) {
error("Can't flash in region mode");
}
parseStatistics.flashCount++;
Integer counter = apertureUseCounts.get(currentAperture.getIndex());
if (counter == null) {
counter = 0;
}
else {
counter++;
}
apertureUseCounts.put(currentAperture.getIndex(), counter);
BoardPad pad = currentAperture.createPad(unit, coordinate);
pad.setName(String.format("D%02d-%03d", currentAperture.getIndex(), counter++));
pads.add(pad);
parseStatistics.padCount++;
currentPoint = coordinate;
parseStatistics.flashPerformedCount++;
}
private void enableRegionMode() throws Exception {
if (regionMode) {
error("Can't start region mode when already in region mode");
}
regionMode = true;
regionStarted = false;
}
private void addRegionLine(Point2D.Double coordinate) throws Exception {
if (!regionMode) {
error("Can't add region line outside of region mode");
}
if (!regionStarted) {
regionStarted = true;
}
parseStatistics.regionLineCount++;
warn("Linear interpolation in region mode not yet supported");
}
private void addRegionArc(Point2D.Double coordinate, Point2D.Double arcCoordinate)
throws Exception {
if (!regionMode) {
error("Can't add region arc outside of region mode");
}
if (!regionStarted) {
regionStarted = true;
}
parseStatistics.regionArcCount++;
warn("Circular interpolation in region mode not yet supported");
}
private void closeRegion() throws Exception {
if (!regionMode) {
error("Can't end region when not in region mode");
}
if (regionStarted) {
regionStarted = false;
parseStatistics.regionCount++;
}
}
private void disableRegionMode() throws Exception {
if (!regionMode) {
error("Can't exit region mode, not in region mode");
}
closeRegion();
regionMode = false;
}
private void readExtendedCodeCommand() throws Exception {
if (read() != '%') {
error("Expected start of extended code command");
}
String code = "" + ((char) read()) + ((char) read());
switch (code) {
case "FS": {
// Sets the ‘Coordinate format’ graphics state parameter. See 4.9.
// These commands are mandatory and must be used only once, in the header of a file.
// Example: FSLAX24Y24
readCoordinateFormat();
break;
}
case "MO": {
// Sets the ‘Unit’ graphics state parameter. See 4.10.
readUnit();
break;
}
case "AD": {
// Assigns a D code number to an aperture definition. See 4.11.
// These commands can be used multiple times. It is recommended to put them in
// header of a file.
readApertureDefinition();
break;
}
case "AM": {
// Defines macro apertures which can be referenced from the AD command. See 4.12.
// TODO: We just ignore them for now.
while (peek() != '%') {
readUntil('*');
read();
}
break;
}
case "SR": {
// Sets the ‘Step and Repeat’ graphics state parameter. See 4.13.
// These commands can be used multiple times over the whole file.
readUntil('*');
read();
break;
}
case "LP": {
// Starts a new level and sets the ‘Level polarity’ graphics state parameter. See
// 4.14.
readUntil('*');
read();
break;
}
case "AS": {
// Deprecated axis select, ignore
readUntil('*');
read();
break;
}
case "IN": {
// Deprecated image name, ignore
readUntil('*');
read();
break;
}
case "IP": {
// Deprecated image polarity, ignore
readUntil('*');
read();
break;
}
case "IR": {
// Deprecated image rotation, ignore
readUntil('*');
read();
break;
}
case "LN": {
// Deprecated level name, ignore
readUntil('*');
read();
break;
}
case "MI": {
// Deprecated mirror image, ignore
readUntil('*');
read();
break;
}
case "OF": {
// Deprecated offset, ignore
readUntil('*');
read();
break;
}
case "SF": {
// Deprecated scale factor, ignore
readUntil('*');
read();
break;
}
default: {
warn("Unknown extended command code " + code);
while (peek() != '%') {
read();
}
}
}
if (read() != '%') {
error("Expected end of extended code command");
}
}
private void readApertureDefinition() throws Exception {
int ch;
if (read() != 'D') {
error("Expected aperture D code");
}
int code = readInteger();
int type = read();
Aperture aperture = null;
switch (type) {
case 'R': {
aperture = readRectangleApertureDefinition(code);
break;
}
case 'C': {
aperture = readCircleApertureDefinition(code);
break;
}
case 'O': {
aperture = readObroundApertureDefinition(code);
break;
}
case 'P': {
aperture = readPolygonApertureDefinition(code);
break;
}
default: {
error(String.format("Unhandled aperture definition type %c, code %d", ((char) type),
code));
}
}
apertures.put(code, aperture);
}
private Aperture readRectangleApertureDefinition(int index) throws Exception {
if (read() != ',') {
error("Expected , in rectangle aperture definition");
}
double width = readDecimal();
if (read() != 'X') {
error("Expected X in rectangle aperture definition");
}
double height = readDecimal();
Double holeDiameter = null;
if (peek() == 'X') {
read();
holeDiameter = readDecimal();
}
if (read() != '*') {
error("Expected end of data block");
}
return new RectangleAperture(index, width, height, holeDiameter);
}
private Aperture readCircleApertureDefinition(int index) throws Exception {
if (read() != ',') {
error("Expected , in circle aperture definition");
}
double diameter = readDecimal();
Double holeDiameter = null;
if (peek() == 'X') {
read();
holeDiameter = readDecimal();
}
if (read() != '*') {
error("Expected end of data block");
}
return new CircleAperture(index, diameter, holeDiameter);
}
private Aperture readObroundApertureDefinition(int index) throws Exception {
if (read() != ',') {
error("Expected , in obround aperture definition");
}
double width = readDecimal();
if (read() != 'X') {
error("Expected X in obround aperture definition");
}
double height = readDecimal();
Double holeDiameter = null;
if (peek() == 'X') {
read();
holeDiameter = readDecimal();
}
if (read() != '*') {
error("Expected end of data block");
}
return new ObroundAperture(index, width, height, holeDiameter);
}
private Aperture readPolygonApertureDefinition(int index) throws Exception {
if (read() != ',') {
error("Expected , in circle aperture definition");
}
double diameter = readDecimal();
if (read() != 'X') {
error("Expected X in polygon aperture definition");
}
int numberOfVertices = readInteger();
Double rotation = null;
if (peek() == 'X') {
read();
rotation = readDecimal();
}
Double holeDiameter = null;
if (peek() == 'X') {
read();
holeDiameter = readDecimal();
}
if (read() != '*') {
error("Expected end of data block");
}
return new PolygonAperture(index, diameter, numberOfVertices, rotation, holeDiameter);
}
private void readUnit() throws Exception {
String unitCode = readString(2);
if (unitCode.equals("MM")) {
unit = LengthUnit.Millimeters;
}
else if (unitCode.equals("IN")) {
unit = LengthUnit.Inches;
}
else {
error("Unknown unit code " + unitCode);
}
if (read() != '*') {
error("Expected end of data block");
}
}
private void readCoordinateFormat() throws Exception {
int ch;
while ("LTAI".indexOf((char) peek()) != -1) {
ch = read();
switch (ch) {
case 'L': {
coordinateFormatTrailingZeroOmission = false;
break;
}
case 'T': {
coordinateFormatTrailingZeroOmission = true;
break;
}
case 'A': {
coordinateFormatIncremental = false;
break;
}
case 'I': {
coordinateFormatIncremental = true;
break;
}
}
}
int xI, xD, yI, yD;
ch = read();
if (ch != 'X') {
error("Expected X coordinate format");
}
xI = read() - '0';
if (xI < 0 || xI > 6) {
warn("Invalid coordinate format, X integer part {}, should be >= 0 && <= 6", xI);
}
xD = read() - '0';
if (xD < 4 || xD > 6) {
warn("Invalid coordinate format, X decimal part {}, should be >= 4 && <= 6", xD);
}
ch = read();
if (ch != 'Y') {
error("Expected Y coordinate format");
}
yI = read() - '0';
if (yI < 0 || yI > 6) {
warn("Invalid coordinate format, Y integer part {}, should be >= 0 && <= 6", yI);
}
yD = read() - '0';
if (yD < 4 || yD > 6) {
warn("Invalid coordinate format, Y decimal part {}, should be >= 4 && <= 6", yD);
}
if (xI != yI || xD != yD) {
error(String.format("Coordinate format X does not match Y: %d.%d != %d.%d", xI, xD, yI,
yD));
}
coordinateFormatIntegerLength = xI;
coordinateFormatDecimalLength = xD;
if (read() != '*') {
error("Expected end of data block");
}
}
private String readUntil(int ch) throws Exception {
StringBuffer sb = new StringBuffer();
while (peek() != ch) {
sb.append((char) read());
}
return sb.toString();
}
private String readString(int length) throws Exception {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
sb.append((char) read());
}
return sb.toString();
}
private double readDecimal() throws Exception {
boolean negative = false;
int ch = peek();
if (ch == '-') {
negative = true;
read();
}
else if (ch == '+') {
read();
}
StringBuffer sb = new StringBuffer();
String allowed = "0123456789.";
while (allowed.indexOf(peek()) != -1) {
sb.append((char) read());
}
return (negative ? -1 : 1) * Double.parseDouble(sb.toString());
}
private int readInteger() throws Exception {
boolean negative = false;
int ch = peek();
if (ch == '-') {
negative = true;
read();
}
else if (ch == '+') {
read();
}
StringBuffer sb = new StringBuffer();
String allowed = "0123456789";
while (allowed.indexOf(peek()) != -1) {
sb.append((char) read());
}
return (negative ? -1 : 1) * Integer.parseInt(sb.toString());
}
private double readCoordinateValue() throws Exception {
if (coordinateFormatIncremental) {
error("Incremental coordinate format not supported");
}
if (coordinateFormatTrailingZeroOmission) {
error("Trailing zero omission not supported");
}
if (coordinateFormatIntegerLength == -1 || coordinateFormatDecimalLength == -1) {
error("Coordinate format not specified.");
}
// Read the value as an integer first, since this will read until it hits
// something that isn't an integer character, then pad it out and then
// break up the components.
int value = readInteger();
String sValue = Integer.toString(Math.abs(value));
while (sValue.length() < coordinateFormatIntegerLength + coordinateFormatDecimalLength) {
sValue = "0" + sValue;
}
String integerPart = sValue.substring(0, coordinateFormatIntegerLength);
String decimalPart = sValue.substring(coordinateFormatIntegerLength,
coordinateFormatIntegerLength + coordinateFormatDecimalLength - 1);
return (value < 0 ? -1 : 1) * Double.parseDouble(integerPart + "." + decimalPart);
}
/**
* Read the next character in the stream, skipping any \r or \n that precede it.
*
* @return
* @throws Exception
*/
private int read() throws Exception {
skipCrLf();
int ch = reader.read();
if (ch == -1) {
error("Unexpected end of stream");
}
return ch;
}
/**
* Peek at the next character in the stream, skipping any \r or \n that precede it.
*
* @return
* @throws Exception
*/
private int peek() throws Exception {
skipCrLf();
return _peek();
}
/**
* Consume any number of \r or \n, stopping when another character is found.
*
* @throws Exception
*/
private void skipCrLf() throws Exception {
while (true) {
int ch = _peek();
if (ch == '\n') {
lineNumber++;
reader.read();
}
else if (ch == '\r') {
reader.read();
}
else {
return;
}
}
}
/**
* Return the next character in the reader without consuming it.
*
* @return
* @throws Exception
*/
private int _peek() throws Exception {
reader.mark(1);
int ch = reader.read();
if (ch == -1) {
error("Unexpected end of stream");
}
reader.reset();
return ch;
}
private void reset() {
unit = null;
currentAperture = null;
currentPoint = new Point2D.Double(0, 0);
levelPolarity = LevelPolarity.Dark;
/*
* This is non-standard, but expected by Eagle, at least. The standard says that
* interpolation mode is undefined at the start of the file but Eagle does not appear to
* send a G01 at any point before it starts sending D01s.
*/
interpolationMode = InterpolationMode.Linear;
stopped = false;
regionMode = false;
coordinateFormatIntegerLength = -1;
coordinateFormatDecimalLength = -1;
coordinateFormatTrailingZeroOmission = false;
coordinateFormatIncremental = false;
apertures = new HashMap<>();
lineNumber = 1;
pads = new ArrayList<>();
regionStarted = false;
apertureUseCounts = new HashMap<>();
parseStatistics = new ParseStatistics();
}
private void warn(String s) {
Logger.warn("WARNING: " + lineNumber + ": " + s);
}
private void warn(String fmt, Object o1) {
Logger.warn("WARNING: " + lineNumber + ": " + fmt, o1);
}
private void warn(String fmt, Object o1, Object o2) {
Logger.warn("WARNING: " + lineNumber + ": " + fmt, o1, o2);
}
private void warn(String fmt, Object[] o) {
Logger.warn("WARNING: " + lineNumber + ": " + fmt, o);
}
private void error(String s) throws Exception {
throw new Exception("ERROR: " + lineNumber + ": " + s);
}
public static void main(String[] args) throws Exception {
HashMap<File, ParseStatistics> results = new HashMap<>();
File[] files = new File("/Users/jason/Desktop/paste_tests").listFiles();
for (File file : files) {
if (file.isDirectory()) {
continue;
}
if (file.getName().equals(".DS_Store")) {
continue;
}
Rs274xParser parser = new Rs274xParser();
try {
parser.parseSolderPastePads(file);
}
catch (Exception e) {
System.out.println(file.getName() + " " + e.getMessage());
}
results.put(file, parser.parseStatistics);
}
ParseStatistics total = new ParseStatistics();
Logger.info("");
Logger.info("");
ArrayList<File> sortedFiles = new ArrayList<>(results.keySet());
Collections.sort(sortedFiles, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
}
});
for (File file : sortedFiles) {
ParseStatistics stats = results.get(file);
total.add(stats);;
Logger.info(String.format("%-32s: %s", file.getName(), stats.toString()));
}
String totalLine = String.format("%-32s: %s", "TOTALS", total.toString());
StringBuffer sb = new StringBuffer();
for (int i = 0; i < totalLine.length(); i++) {
sb.append("-");
}
Logger.info(sb.toString());
Logger.info(totalLine);
}
static abstract class Aperture {
final protected int index;
public Aperture(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public abstract BoardPad createPad(LengthUnit unit, Point2D.Double coordinate);
}
static abstract class StandardAperture extends Aperture {
public StandardAperture(int index) {
super(index);
}
}
static class RectangleAperture extends StandardAperture {
public double width;
public double height;
public Double holeDiameter;
public RectangleAperture(int index, double width, double height, Double holeDiameter) {
super(index);
this.width = width;
this.height = height;
this.holeDiameter = holeDiameter;
}
public BoardPad createPad(LengthUnit unit, Point2D.Double coordinate) {
Pad.RoundRectangle pad = new Pad.RoundRectangle();
pad.setUnits(unit);
pad.setWidth(width);
pad.setHeight(height);
pad.setRoundness(0);
BoardPad boardPad =
new BoardPad(pad, new Location(unit, coordinate.x, coordinate.y, 0, 0));
return boardPad;
}
@Override
public String toString() {
return "RectangleAperture [width=" + width + ", height=" + height + ", holeDiameter="
+ holeDiameter + "]";
}
}
static class CircleAperture extends StandardAperture {
public double diameter;
public Double holeDiameter;
public CircleAperture(int index, double diameter, Double holeDiameter) {
super(index);
this.diameter = diameter;
this.holeDiameter = holeDiameter;
}
public BoardPad createPad(LengthUnit unit, Point2D.Double coordinate) {
Pad.Circle pad = new Pad.Circle();
pad.setRadius(diameter / 2);
pad.setUnits(unit);
BoardPad boardPad =
new BoardPad(pad, new Location(unit, coordinate.x, coordinate.y, 0, 0));
return boardPad;
}
@Override
public String toString() {
return "CircleAperture [diameter=" + diameter + ", holeDiameter=" + holeDiameter + "]";
}
}
static class ObroundAperture extends RectangleAperture {
public ObroundAperture(int index, double width, double height, Double holeDiameter) {
super(index, width, height, holeDiameter);
}
@Override
public String toString() {
return "ObroundAperture [width=" + width + ", height=" + height + ", holeDiameter="
+ holeDiameter + "]";
}
}
static class PolygonAperture extends CircleAperture {
public int numberOfVertices;
public Double rotation;
public PolygonAperture(int index, double diameter, int numberOfVertices, Double rotation,
Double holeDiameter) {
super(index, diameter, holeDiameter);
this.numberOfVertices = numberOfVertices;
this.rotation = rotation;
}
@Override
public String toString() {
return "PolygonAperture [numberOfVertices=" + numberOfVertices + ", rotation="
+ rotation + ", diameter=" + diameter + ", holeDiameter=" + holeDiameter + "]";
}
}
static class MacroAperture extends Aperture {
public MacroAperture(int index) {
super(index);
}
@Override
public BoardPad createPad(LengthUnit unit, java.awt.geom.Point2D.Double coordinate) {
return null;
}
}
static class ParseStatistics {
public int lineCount;
public int linePerformedCount;
public int arcCount;
public int arcPerformedCount;
public int regionLineCount;
public int regionLinePerformedCount;
public int regionArcCount;
public int regionArcPerformedCount;
public int regionCount;
public int regionPerformedCount;
public int flashCount;
public int flashPerformedCount;
public int padCount;
public boolean errored;
public double percent(double count, double total) {
if (total == 0) {
return 100;
}
return (count / total) * 100;
}
public void add(ParseStatistics p) {
lineCount += p.lineCount;
linePerformedCount += p.linePerformedCount;
arcCount += p.arcCount;
arcPerformedCount += p.arcPerformedCount;
regionLineCount += p.regionLineCount;
regionLinePerformedCount += p.regionLinePerformedCount;
regionArcCount += p.regionArcCount;
regionArcPerformedCount += p.regionArcPerformedCount;
regionCount += p.regionCount;
regionPerformedCount += p.regionPerformedCount;
flashCount += p.flashCount;
flashPerformedCount += p.flashPerformedCount;
padCount += p.padCount;
}
@Override
public String toString() {
int total = flashCount + regionCount + lineCount + arcCount;
int totalPerformed = flashPerformedCount + regionPerformedCount + linePerformedCount
+ arcPerformedCount;
return String.format(
"%s Total %3.0f%% (%4d/%4d), Flash %3.0f%% (%4d/%4d), Line %3.0f%% (%4d/%4d), Arc %3.0f%% (%4d/%4d), Region %3.0f%% (%4d/%4d), Region line %3.0f%% (%4d/%4d), Region Arc %3.0f%% (%4d/%4d), Pads %4d",
errored ? "FAIL" : "PASS", percent(totalPerformed, total), totalPerformed,
total, percent(flashPerformedCount, flashCount), flashPerformedCount,
flashCount, percent(linePerformedCount, lineCount), linePerformedCount,
lineCount, percent(arcPerformedCount, arcCount), arcPerformedCount, arcCount,
percent(regionPerformedCount, regionCount), regionPerformedCount, regionCount,
percent(regionLinePerformedCount, regionLineCount), regionLinePerformedCount,
regionLineCount, percent(regionArcPerformedCount, regionArcCount),
regionArcPerformedCount, regionArcCount, padCount);
}
}
}