/*
* Copyright (C) 2015 Douglas Pearless <Douglas.Pearless@gmail.com>
*
* This file is part of OpenPnP.
*
* OpenPnP is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* OpenPnP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with OpenPnP. If not, see
* <http://www.gnu.org/licenses/>.
*
* For more information about OpenPnP visit http://openpnp.org
*/
package org.openpnp.gui.importer;
import java.awt.FileDialog;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JSeparator;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.border.TitledBorder;
import org.openpnp.gui.support.MessageBoxes;
import org.openpnp.model.Board;
import org.openpnp.model.Board.Side;
import org.openpnp.model.BoardPad;
import org.openpnp.model.Configuration;
import org.openpnp.model.LengthUnit;
import org.openpnp.model.Location;
import org.openpnp.model.Package;
import org.openpnp.model.Pad;
import org.openpnp.model.Part;
import org.openpnp.model.Placement;
import org.openpnp.model.Point;
import org.openpnp.model.eagle.EagleLoader;
import org.openpnp.model.eagle.xml.Element;
import org.openpnp.model.eagle.xml.Layer;
import org.openpnp.model.eagle.xml.Library;
import org.openpnp.model.eagle.xml.Param;
import org.openpnp.model.eagle.xml.Vertex;
import org.openpnp.util.Utils2D;
import org.pmw.tinylog.Logger;
import com.jgoodies.forms.layout.ColumnSpec;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.FormSpecs;
import com.jgoodies.forms.layout.RowSpec;
@SuppressWarnings("serial")
public class EagleBoardImporter implements BoardImporter {
private final static String NAME = "CadSoft EAGLE Board";
private final static String DESCRIPTION =
"Import files directly from EAGLE's <filename>.brd file.";
private static Board board;
private File boardFile;
static private Double mil_to_mm = 0.0254;
@Override
public String getImporterName() {
return NAME;
}
@Override
public String getImporterDescription() {
return DESCRIPTION;
}
@Override
public Board importBoard(Frame parent) throws Exception {
Dlg dlg = new Dlg(parent);
dlg.setVisible(true);
return board;
}
private static List<Placement> parseFile(File file, Side side, boolean createMissingParts)
throws Exception {
String dimensionLayer = "";
String topLayer = "";
String bottomLayer = "";
String tCreamLayer = "";
String bCreamLayer = "";
String mmMinCreamFrame_string;
double mmMinCreamFrame_number = 0;
String mmMaxCreamFrame_string;
double mmMaxCreamFrame_number = 0;
String libraryId = "";
String packageId = "";
Part part = null;
List<BoardPad> pads = new ArrayList<>();
ArrayList<Placement> placements = new ArrayList<>();
// we don't use the 'side' parameter as we can read this from the .brd file
// in the future we could use the side parameter to restrict this from only parsing one side
// or the other or both
EagleLoader boardToProcess = new EagleLoader(file);
if (boardToProcess.board != null) {
// first establish which is the Dimension, Top, Bottom, tCream and bCream layers in case
// the board has non-standard layer numbering
for (Layer layer : boardToProcess.layers.getLayer()) {
if (layer.getName().equalsIgnoreCase("Dimension")) {
dimensionLayer = layer.getNumber();
}
else if (layer.getName().equalsIgnoreCase("Top")) {
topLayer = layer.getNumber();
}
else if (layer.getName().equalsIgnoreCase("Bottom")) {
bottomLayer = layer.getNumber();
}
else if (layer.getName().equalsIgnoreCase("tCream")) {
tCreamLayer = layer.getNumber();
}
else if (layer.getName().equalsIgnoreCase("bCream")) {
bCreamLayer = layer.getNumber();
}
}
// Now we want to establish the width of the board which we need to record
Double x_boundary = 0.0;
for (Object e : boardToProcess.board.getPlain()
.getPolygonOrWireOrTextOrDimensionOrCircleOrRectangleOrFrameOrHole()) {
if (e instanceof org.openpnp.model.eagle.xml.Wire) {
if (((org.openpnp.model.eagle.xml.Wire) e).getLayer()
.equalsIgnoreCase(dimensionLayer)) {
x_boundary = Math.max(x_boundary,
Double.parseDouble(((org.openpnp.model.eagle.xml.Wire) e).getX1()));
x_boundary = Math.max(x_boundary,
Double.parseDouble(((org.openpnp.model.eagle.xml.Wire) e).getX2()));
}
}
}
Point center = new Point(x_boundary / 2, 0); // note that we set x = maximum x point on
// the Y=0;
// determine the parameters for the pads based on DesignRules
for (Param params : boardToProcess.board.getDesignrules().getParam()) {
if (params.getName().compareToIgnoreCase("mlMinCreamFrame") == 0) { // found exact
// match when 0
// returned
mmMinCreamFrame_string = params.getValue().replaceAll("[A-Za-z ]", ""); // remove
// all
// letters,
// i.e.
// 0mil
// becomes
// 0
if (params.getValue().toUpperCase().endsWith("MIL")) {
mmMinCreamFrame_number =
Double.parseDouble(mmMinCreamFrame_string) * mil_to_mm;
}
else if (params.getValue().toUpperCase().endsWith("MM")) {
mmMinCreamFrame_number =
Double.parseDouble(mmMinCreamFrame_string) * mil_to_mm;
}
else
throw new Exception("mlMinCream must either be in mil or mm"); // Force the
// importer
// to abort,
// something
// is very
// wrong
}
if (params.getName().compareToIgnoreCase("mlMaxCreamFrame") == 0) { // found exact
// match when 0
// returned
mmMaxCreamFrame_string = params.getValue().replaceAll("[A-Za-z ]", ""); // remove
// all
// letters,
// i.e.
// "0mil"
// becomes
// 0
if (params.getValue().toUpperCase().endsWith("MIL")) {
mmMaxCreamFrame_number =
Double.parseDouble(mmMaxCreamFrame_string) * mil_to_mm;
}
else if (params.getValue().toUpperCase().endsWith("MM")) {
mmMaxCreamFrame_number = Double.parseDouble(mmMaxCreamFrame_string);
}
else
throw new Exception("mlMaxCream must either be in mil or mm"); // Force the
// importer
// to abort,
// something
// is very
// wrong
}
}
// Now we know the min and max tolerance for the cream (aka solder paste)
// which are mmMinCreamFrame_number and mmMaxCreamFrame_number and are in mm (converted
// from mil as required)
// Now we got through each of the parts
if (!boardToProcess.board.getElements().getElement().isEmpty()) {
// Process each of the element items
for (Element element : boardToProcess.board.getElements().getElement()) {
// first we determine if the part is on the top layer or bottom layer
Side element_side;
String rot = element.getRot();
if (rot.toUpperCase().startsWith("M"))
// The part is mirrored and therefore is on the bottom of the board
element_side = Side.Bottom;
else
element_side = Side.Top;
// Now determine if we want to process this part based on which side of the
// board it is on
if (side != null) { // null means process both sides
if (side != element_side)
continue; // exit this loop and process the next element
}
String rot_number = rot.replaceAll("[A-Za-z ]", ""); // remove all letters, i.e.
// R180 becomes 180
Placement placement = new Placement(element.getName());
double rotation = Double.parseDouble(rot_number);
double x = Double.parseDouble(element.getX());
double y = Double.parseDouble(element.getY());
placement.setLocation(new Location(LengthUnit.Millimeters, x, y, 0, rotation));
// placement now contains where the package is on the PCB, we need to work out
// where the pads
// are relative to the 'placement'
Configuration cfg = Configuration.get();
if (cfg != null && createMissingParts) {
String value = element.getValue(); // Value
packageId = element.getPackage(); // Package
libraryId = element.getLibrary(); // Library that contains the package
String pkgId = libraryId + "-" + packageId;
String partId = libraryId + "-" + packageId;
if (value.trim().length() > 0) {
partId += "-" + value;
}
part = cfg.getPart(partId);
Package pkg = cfg.getPackage(pkgId);
if ((part == null) || (pkg == null)) {
if (pkg == null) {
pkg = new Package(pkgId);
cfg.addPackage(pkg); // save the package in the configuration file
if (part != null) {
cfg.removePart(part);// we have to remove the part so we can
// re-add it with the correct package &
// library
part = null;
}
}
if (part == null) {
part = new Part(partId);
part.setPackage(pkg);
// TODO part.setLibrary(libraryId);
cfg.addPart(part); // save the package in the configuration file
}
cfg.addPart(part);
}
}
placement.setPart(part);
// Now we have the part, we now need to add the SolderPastePad to the board
// Note, Eagle has the concept of minimum and max from the edge of the pad so we
// need to
// adjust the pad to be the size as the mid-point between the minimum and max
// in practice these are usually 0, which means we paste the entire pad
if (!boardToProcess.board.getLibraries().getLibrary().isEmpty()) {
for (Library library : boardToProcess.board.getLibraries().getLibrary()) {
if (library.getName().equalsIgnoreCase(libraryId)) {
// we have found the library, now to scan for the package we want
if (!library.getPackages().getPackage().isEmpty()) {
ListIterator<org.openpnp.model.eagle.xml.Package> it =
library.getPackages().getPackage().listIterator();
while (it.hasNext()) {
org.openpnp.model.eagle.xml.Package pak =
(org.openpnp.model.eagle.xml.Package) it.next();
if (pak.getName().equalsIgnoreCase(packageId)) {
for (Object e : pak
.getPolygonOrWireOrTextOrDimensionOrCircleOrRectangleOrFrameOrHoleOrPadOrSmd()) {
if (e instanceof org.openpnp.model.eagle.xml.Smd) {
// we have found the correct package in the
// correct library and we need to to add the pad
// to the boardPads
if (!((org.openpnp.model.eagle.xml.Smd) e)
.getCream().equalsIgnoreCase("No")) { // if
// cream="no"
// then
// we
// do
// not
// paste
// this
// pad
Pad.RoundRectangle pad =
new Pad.RoundRectangle();
pad.setUnits(LengthUnit.Millimeters);
// TODO check that these reduce the pad to
// the halfway between the minimum & maximum
// tolerances
pad.setHeight(Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getDx())
- (mmMaxCreamFrame_number
- mmMinCreamFrame_number)
/ 2);
pad.setWidth(Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getDy())
- (mmMaxCreamFrame_number
- mmMinCreamFrame_number)
/ 2);
pad.setRoundness(0);
pad.setRoundness(Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getRoundness()));
// first find out how is the package defined
Double pad_rotation =
Double.parseDouble(rot_number);
// now rotate the pad by its own rotation
// relative to its origin and make sure we
// don't turn through 360 degrees
pad_rotation += Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getRot().replaceAll(
"[A-Za-z ]", ""))
% 360;
Point A = new Point(
Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getX())
+ x,
Double.parseDouble(
((org.openpnp.model.eagle.xml.Smd) e)
.getY())
+ y);
Point part_center = new Point(x, y);
if (element_side == Side.Top) {
if (rotation > 180)
A = Utils2D
.rotateTranslateCenterPoint(
A, rotation, 0, 0,
part_center); // rotate
// the
// part-pin
else
A = Utils2D
.rotateTranslateCenterPoint(
A, -rotation, 0, 0,
part_center); // rotate
// the
// part-pin
}
else if (element_side == Side.Bottom) {
if (rotation > 180)
A = Utils2D
.rotateTranslateCenterPoint(
A, rotation, 0, 0,
part_center); // rotate
// the
// part-pin
else
A = Utils2D
.rotateTranslateCenterPoint(
A,
-(180 - rotation),
0, 0, part_center); // rotate
// the
// part-pin
// Mirror along the Y axis of the board
if (A.getX() < center.getX()) {
Double offset =
center.getX() - A.getX();
A.setX(center.getX() + offset); // mirror
// left
// to
// right
// across
// the
// centre
// of
// the
// board
}
else {
Double offset =
A.getX() - center.getX();
A.setX(center.getX() - offset);
}
// Mirror along the X axis of the part's
// center line
if (A.getY() < y) {
Double offset = y - A.getY();
A.setY(y + offset); // mirror top to
// bottom across
// the centre of
// the part
}
else {
Double offset = A.getY() - y;
A.setY(y - offset); // mirror bottom
// to top across
// the centre of
// the part
}
}
// TODO Need to write the logic for pad
// rotation
// A = Utils2D.rotateTranslateCenterPoint(A,
// pad_rotation,0,0,center);
//
BoardPad boardPad = new BoardPad(pad,
new Location(LengthUnit.Millimeters,
A.getX(), A.getY(), 0,
pad_rotation));
// TODO add support for Circle pads
boardPad.setName(element.getName() + "-"
+ ((org.openpnp.model.eagle.xml.Smd) e)
.getName());
if (((org.openpnp.model.eagle.xml.Smd) e)
.getLayer()
.equalsIgnoreCase(topLayer)) { // is
// the
// pad
// on
// top
if (element_side == Side.Top) // part is
// on the
// top
boardPad.setSide(Side.Top); // pad
// is on
// the
// top
else
boardPad.setSide(Side.Bottom); // part
// is
// on
// top,
// but
// pat
// is
// on
// the
// bottom
}
else if (((org.openpnp.model.eagle.xml.Smd) e)
.getLayer()
.equalsIgnoreCase(bottomLayer)) { // is
// the
// pad
// on
// the
// bottom
if (element_side == Side.Top) // part is
// top
boardPad.setSide(Side.Bottom); // pad
// stays
// on
// the
// bottom
else
boardPad.setSide(Side.Top); // pad
// moves
// to
// the
// top
}
else
Logger.info("Warning: " + file
+ "contains a SMD pad that is not on a topLayer or bottomLayer");
// TODO figure out if it is possible for an
// SMD pad to have a drill, it appears not
// !!
// pad.setdrillDiameter(0);
// TODO later we need to associate a list of
// pads to a board.
pads.add(boardPad);
board.addSolderPastePad(boardPad); // This
// adds
// the
// pad to
// the
// SolderPaste
}
}
else if (e instanceof org.openpnp.model.eagle.xml.Pad) {
// TODO implement pasting for through hole pads
}
else if (e instanceof org.openpnp.model.eagle.xml.Polygon) {
// We have a polygon is it on a tCream or bCream
// layer, otherwise ignore it
if (((org.openpnp.model.eagle.xml.Polygon) e)
.getLayer()
.equalsIgnoreCase(tCreamLayer)
|| ((org.openpnp.model.eagle.xml.Polygon) e)
.getLayer().equalsIgnoreCase(
bCreamLayer)) {
Logger.info("Warning: " + file
+ " contains a Polygon pad - this functionality has been implmented as the smallest bounded rectangle and may over paste the area");
Logger.info(
"Layer" + ((org.openpnp.model.eagle.xml.Polygon) e)
.getLayer().toString());
Double vertex_x_min = 0.0;
Double vertex_x_max = 0.0;
Double vertex_y_min = 0.0;
Double vertex_y_max = 0.0;
ListIterator<org.openpnp.model.eagle.xml.Vertex> vertex_it =
((org.openpnp.model.eagle.xml.Polygon) e)
.getVertex().listIterator();
while (vertex_it.hasNext()) {
org.openpnp.model.eagle.xml.Vertex vertex =
(Vertex) vertex_it.next();
vertex_x_min = Math.min(vertex_x_min,
Double.parseDouble(
vertex.getX()));
vertex_x_max = Math.max(vertex_x_max,
Double.parseDouble(
vertex.getX()));
vertex_y_min = Math.min(vertex_y_min,
Double.parseDouble(
vertex.getY()));
vertex_y_max = Math.max(vertex_y_max,
Double.parseDouble(
vertex.getY()));
Logger.info("Vertex: X=" + vertex.getX()
+ " y=" + vertex.getY());
}
// TODO implement polygon pad in Pad.java
Pad.RoundRectangle pad =
new Pad.RoundRectangle();
pad.setUnits(LengthUnit.Millimeters);
pad.setRoundness(0);
pad.setHeight(
(vertex_y_max - vertex_y_min));
pad.setWidth((vertex_x_max - vertex_x_min));
BoardPad boardPad = new BoardPad(pad,
new Location(LengthUnit.Millimeters,
x + (vertex_x_max
+ vertex_x_min) / 2,
y + (vertex_y_max
+ vertex_y_min) / 2,
0, 0));
Logger.info("Pad generated width is "
+ pad.getWidth() + " height "
+ pad.getHeight()
+ " centered at x = "
+ boardPad.getLocation().getX()
+ " y = "
+ boardPad.getLocation().getY());
boardPad.setName(element.getName() + "-"
+ "Polygon "); // Polygons are not
// named so just name
// it as "Polygon"
if (((org.openpnp.model.eagle.xml.Polygon) e)
.getLayer()
.equalsIgnoreCase(tCreamLayer))
boardPad.setSide(Side.Top);
else
boardPad.setSide(Side.Bottom);
pads.add(boardPad);
board.addSolderPastePad(boardPad); // This
// adds
// the
// pad to
// the
// SolderPaste
}
}
}
}
}
}
}
}
}
placement.setSide(element_side);
placements.add(placement);
board.addPlacement(placement); // this adds the placement to the Pick and Place
// list
}
}
}
if (boardToProcess.library != null) {
}
if (boardToProcess.schematic != null) {
}
return placements;
}
class Dlg extends JDialog {
private JTextField textFieldBoardFile;
private final Action browseBoardFileAction = new SwingAction();
private final Action importAction = new SwingAction_2();
private final Action cancelAction = new SwingAction_3();
private JCheckBox chckbxCreateMissingParts;
private JCheckBox chckbxImportTop;
private JCheckBox chckbxImportBottom;
public Dlg(Frame parent) {
super(parent, DESCRIPTION, true);
getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
JPanel panel = new JPanel();
panel.setBorder(new TitledBorder(null, "Files", TitledBorder.LEADING, TitledBorder.TOP,
null, null));
getContentPane().add(panel);
panel.setLayout(new FormLayout(
new ColumnSpec[] {FormSpecs.RELATED_GAP_COLSPEC, FormSpecs.DEFAULT_COLSPEC,
FormSpecs.RELATED_GAP_COLSPEC, ColumnSpec.decode("default:grow"),
FormSpecs.RELATED_GAP_COLSPEC, FormSpecs.DEFAULT_COLSPEC,},
new RowSpec[] {FormSpecs.RELATED_GAP_ROWSPEC, FormSpecs.DEFAULT_ROWSPEC,
FormSpecs.RELATED_GAP_ROWSPEC, FormSpecs.DEFAULT_ROWSPEC,}));
JLabel lblBoardFilebrd = new JLabel("Eagle PCB Board File (.brd)");
panel.add(lblBoardFilebrd, "2, 2, right, default");
textFieldBoardFile = new JTextField();
panel.add(textFieldBoardFile, "4, 2, fill, default");
textFieldBoardFile.setColumns(10);
JButton btnBrowse = new JButton("Browse");
btnBrowse.setAction(browseBoardFileAction);
panel.add(btnBrowse, "6, 2");
JPanel panel_1 = new JPanel();
panel_1.setBorder(new TitledBorder(null, "Options", TitledBorder.LEADING,
TitledBorder.TOP, null, null));
getContentPane().add(panel_1);
panel_1.setLayout(new FormLayout(
new ColumnSpec[] {FormSpecs.RELATED_GAP_COLSPEC, FormSpecs.DEFAULT_COLSPEC,},
new RowSpec[] {FormSpecs.RELATED_GAP_ROWSPEC, FormSpecs.DEFAULT_ROWSPEC,
FormSpecs.RELATED_GAP_ROWSPEC, FormSpecs.DEFAULT_ROWSPEC,
FormSpecs.RELATED_GAP_ROWSPEC, FormSpecs.DEFAULT_ROWSPEC,}));
chckbxCreateMissingParts = new JCheckBox("Create Missing Parts");
chckbxCreateMissingParts.setSelected(true);
panel_1.add(chckbxCreateMissingParts, "2, 2");
chckbxImportTop = new JCheckBox("Import Parts on the Top of the board");
chckbxImportTop.setSelected(true);
panel_1.add(chckbxImportTop, "2, 4");
chckbxImportBottom = new JCheckBox("Import Parts on the Bottom of the board");
chckbxImportBottom.setSelected(true);
panel_1.add(chckbxImportBottom, "2, 6");
JSeparator separator = new JSeparator();
getContentPane().add(separator);
JPanel panel_2 = new JPanel();
FlowLayout flowLayout = (FlowLayout) panel_2.getLayout();
flowLayout.setAlignment(FlowLayout.RIGHT);
getContentPane().add(panel_2);
JButton btnCancel = new JButton("Cancel");
btnCancel.setAction(cancelAction);
panel_2.add(btnCancel);
JButton btnImport = new JButton("Import");
btnImport.setAction(importAction);
panel_2.add(btnImport);
setSize(400, 400);
setLocationRelativeTo(parent);
JRootPane rootPane = getRootPane();
KeyStroke stroke = KeyStroke.getKeyStroke("ESCAPE");
InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
inputMap.put(stroke, "ESCAPE");
rootPane.getActionMap().put("ESCAPE", cancelAction);
}
private class SwingAction extends AbstractAction {
public SwingAction() {
putValue(NAME, "Browse");
putValue(SHORT_DESCRIPTION, "Browse");
}
public void actionPerformed(ActionEvent e) {
FileDialog fileDialog = new FileDialog(Dlg.this);
fileDialog.setFilenameFilter(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".brd");
}
});
fileDialog.setVisible(true);
if (fileDialog.getFile() == null) {
return;
}
File file = new File(new File(fileDialog.getDirectory()), fileDialog.getFile());
textFieldBoardFile.setText(file.getAbsolutePath());
}
}
private class SwingAction_2 extends AbstractAction {
public SwingAction_2() {
putValue(NAME, "Import");
putValue(SHORT_DESCRIPTION, "Import");
}
public void actionPerformed(ActionEvent e) {
boardFile = new File(textFieldBoardFile.getText());
board = new Board();
List<Placement> placements = new ArrayList<>();
try {
if (boardFile.exists()) {
if (chckbxImportTop.isSelected() && chckbxImportBottom.isSelected())
placements.addAll(parseFile(boardFile, null,
chckbxCreateMissingParts.isSelected())); // both Top and Bottom
// of the board
else if (chckbxImportTop.isSelected())
placements.addAll(parseFile(boardFile, Side.Top,
chckbxCreateMissingParts.isSelected())); // Just the Top side of
// the board
else if (chckbxImportBottom.isSelected())
placements.addAll(parseFile(boardFile, Side.Bottom,
chckbxCreateMissingParts.isSelected())); // Just the Bottom side
// of the board
}
}
catch (Exception e1) {
MessageBoxes.errorBox(Dlg.this, "Import Error", e1);
return;
}
setVisible(false);
}
}
private class SwingAction_3 extends AbstractAction {
public SwingAction_3() {
putValue(NAME, "Cancel");
putValue(SHORT_DESCRIPTION, "Cancel");
}
public void actionPerformed(ActionEvent e) {
setVisible(false);
}
}
}
}