package teamcomm.gui.drawings; import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2; import com.jogamp.opengl.glu.GLU; import com.jogamp.opengl.glu.GLUquadric; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.FloatBuffer; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.Characters; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import teamcomm.gui.drawings.TextureLoader.Texture; /** * Class for parsing ros2 scene files. Instances of this class correspond to * named scene elements and can be instantiated to RoSi2Drawabled which may be * drawn in a OpenGL context. * * @author Felix Thielke */ public class RoSi2Element { /** * References to all named elements */ private final Map<String, RoSi2Element> namedElements; /** * Path of the file in which this element is declared. */ private final File filepath; /** * Immediate children of this element. */ private final List<RoSi2Element> children = new LinkedList<>(); /** * Set variable bindings for this element and its children. */ private final Map<String, String> vars = new HashMap<>(); /** * Attributes of this element. */ private final Map<String, String> attributes = new HashMap<>(); /** * Name of this element or null if it is unnamed. */ private final String name; /** * Tag of this element. */ private final String tag; /** * Textual content of this element. */ private final StringBuilder content = new StringBuilder(); /** * Drawable instance of this element. Only set if the element is constant, * i.e. it and its children reference no variables, and it was instantiated * as a drawable at least once. */ private RoSi2Drawable constantInstance; private RoSi2Element(final File path, final String tag, final Map<String, RoSi2Element> namedElements) { this(path, tag, null, null, namedElements); } private RoSi2Element(final File path, final String tag, final String name, final Iterator<Attribute> iter, final Map<String, RoSi2Element> namedElements) { this.filepath = path; this.tag = tag; this.name = name; this.namedElements = namedElements; if (iter != null) { while (iter.hasNext()) { final Attribute attr = iter.next(); attributes.put(attr.getName().getLocalPart(), attr.getValue()); } } } /** * Returns the name of this element. * * @return name */ public String getName() { return name; } /** * Returns the child element with the given name from the element. Elements * are searched via breadth-first search. * * @param name Name of the child * @return Child element or null if no matching element was found */ public RoSi2Element findElement(final String name) { if (name == null) { return null; } final LinkedList<RoSi2Element> elems = new LinkedList<>(children); while (!elems.isEmpty()) { final RoSi2Element cur = elems.pollFirst(); if (name.equals(cur.name)) { return cur; } else { elems.addAll(cur.children); } } return null; } /** * Returns all child elements with one of the given names from the element. * Elements are searched via breadth-first search. * * @param names Names of the children * @return List containing found elements */ public List<RoSi2Element> findElements(final Collection<String> names) { final List<RoSi2Element> foundElems = new LinkedList<>(); if (names.isEmpty()) { return foundElems; } final Set<String> searchedNames = new HashSet<>(names); final LinkedList<RoSi2Element> elems = new LinkedList<>(children); while (!elems.isEmpty()) { final RoSi2Element cur = elems.pollFirst(); if (cur.name != null && searchedNames.contains(cur.name)) { foundElems.add(cur); searchedNames.remove(cur.name); if (searchedNames.isEmpty()) { return foundElems; } } elems.addAll(cur.children); } return foundElems; } /** * Instantiates this element as a drawable on the given OpenGL context. * * @param gl OpenGL context * @return drawable * @throws teamcomm.gui.drawings.RoSi2Element.RoSi2ParseException if * instantiated attributes of the element could not be parsed */ public RoSi2Drawable instantiate(final GL2 gl) throws RoSi2ParseException { return instantiate(gl, null); } /** * Instantiates this element as a drawable on the given OpenGL context. * * @param gl OpenGL context * @param vars variable assignments to use * @return drawable * @throws teamcomm.gui.drawings.RoSi2Element.RoSi2ParseException if * instantiated attributes of the element could not be parsed */ public RoSi2Drawable instantiate(final GL2 gl, final Map<String, String> vars) throws RoSi2ParseException { return instantiate(gl, vars, null); } private RoSi2Drawable instantiate(final GL2 gl, final Map<String, String> vars, final List<RoSi2Drawable> refChilds) throws RoSi2ParseException { // The instantiation is constant unless it references a variable boolean constant = true; // Return the constant instance if it exists if (constantInstance != null) { return constantInstance; } // Merge the given variable bindings with those of the element final Map<String, String> varBindings; if (vars != null) { varBindings = vars; for (final Map.Entry<String, String> entry : this.vars.entrySet()) { if (!varBindings.containsKey(entry.getKey())) { varBindings.put(entry.getKey(), entry.getValue()); } } } else { varBindings = new HashMap<>(this.vars); } // Instantiate all child elements final List<RoSi2Drawable> childInstances = (refChilds != null) ? new LinkedList<>(refChilds) : new LinkedList<RoSi2Drawable>(); for (final RoSi2Element child : children) { final RoSi2Drawable childInst = child.instantiate(gl, varBindings, null); if (childInst != null) { // If a child instance is not constant, the instance of this // element is neither if (childInst != child.constantInstance) { constant = false; } childInstances.add(childInst); } } // Check if this element references another and in that case instantiate // the referenced element final String ref = getAttributeValue(varBindings, "ref", false); if (ref != null) { final RoSi2Element referenced = namedElements.get(tag + "#" + ref); if (referenced == null) { throw new RoSi2ParseException("Referenced element cannot be found: " + ref); } return referenced.instantiate(gl, varBindings, childInstances); } // Instantiate this element final RoSi2Drawable instance; switch (tag) { case "Compound": instance = new Compound(gl, childInstances); break; case "Body": instance = new Body(gl, childInstances); break; case "Translation": instance = new Translation(new float[]{ getLength(varBindings, "x", false, 0.0f), getLength(varBindings, "y", false, 0.0f), getLength(varBindings, "z", false, 0.0f) }); break; case "Rotation": instance = new Rotation(new float[]{ getAngle(varBindings, "x", false, 0.0f), getAngle(varBindings, "y", false, 0.0f), getAngle(varBindings, "z", false, 0.0f) }); break; case "Appearance": instance = new Appearance(gl, childInstances); break; case "BoxAppearance": instance = new BoxAppearance(gl, childInstances, getLength(varBindings, "width", true, 0.0f), getLength(varBindings, "height", true, 0.0f), getLength(varBindings, "depth", true, 0.0f)); break; case "SphereAppearance": instance = new SphereAppearance(gl, childInstances, getLength(varBindings, "radius", true, 0.0f)); break; case "CylinderAppearance": instance = new CylinderAppearance(gl, childInstances, getLength(varBindings, "height", true, 0.0f), getLength(varBindings, "radius", true, 0.0f)); break; case "CapsuleAppearance": instance = new CapsuleAppearance(gl, childInstances, getLength(varBindings, "height", true, 0.0f), getLength(varBindings, "radius", true, 0.0f)); break; case "ComplexAppearance": instance = new ComplexAppearance(gl, childInstances); break; case "Vertices": { final double unit = getUnit(varBindings, "unit", false, 1.0f); final ArrayList<Vertices.Vertex> vs = new ArrayList<>(); final String str = content.toString(); final DecimalFormat fmt = new DecimalFormat(); fmt.setGroupingUsed(false); final DecimalFormatSymbols sym = (DecimalFormatSymbols) DecimalFormatSymbols.getInstance().clone(); sym.setDecimalSeparator('.'); fmt.setDecimalFormatSymbols(sym); int pos = 0; final double[] vertex = new double[3]; int i = 0; // parse vertices parsing: while (pos < str.length()) { while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) == '#') { do { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) != '\n' && str.charAt(pos) != '\r'); while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } } final ParsePosition p = new ParsePosition(pos); final Number n = fmt.parse(str, p); if (n == null) { throw new RoSi2ParseException("Vertex coordinate is not a number: " + str.substring(pos, pos + 1)); } pos = p.getIndex(); vertex[i++] = n.doubleValue(); if (i == 3) { vs.add(new Vertices.Vertex((float) (vertex[0] * unit), (float) (vertex[1] * unit), (float) (vertex[2] * unit))); i = 0; } } vs.trimToSize(); instance = new Vertices(vs); break; } case "Normals": { final ArrayList<Normals.Normal> ns = new ArrayList<>(); final String str = content.toString(); final DecimalFormat fmt = new DecimalFormat(); fmt.setGroupingUsed(false); final DecimalFormatSymbols sym = (DecimalFormatSymbols) DecimalFormatSymbols.getInstance().clone(); sym.setDecimalSeparator('.'); fmt.setDecimalFormatSymbols(sym); int pos = 0; final float[] normal = new float[3]; int i = 0; // parse normals parsing: while (pos < str.length()) { while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) == '#') { do { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) != '\n' && str.charAt(pos) != '\r'); while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } } final ParsePosition p = new ParsePosition(pos); final Number n = fmt.parse(str, p); if (n == null) { throw new RoSi2ParseException("Normal coordinate is not a number"); } normal[i++] = n.floatValue(); pos = p.getIndex(); if (i == 3) { ns.add(new Normals.Normal(normal[0], normal[1], normal[2], 1)); i = 0; } } ns.trimToSize(); instance = new Normals(ns); break; } case "TexCoords": { final ArrayList<TexCoords.TexCoord> ts = new ArrayList<>(); final String str = content.toString(); final DecimalFormat fmt = new DecimalFormat(); fmt.setGroupingUsed(false); final DecimalFormatSymbols sym = (DecimalFormatSymbols) DecimalFormatSymbols.getInstance().clone(); sym.setDecimalSeparator('.'); fmt.setDecimalFormatSymbols(sym); int pos = 0; final float[] coord = new float[2]; int i = 0; // parse texture coordinates parsing: while (pos < str.length()) { while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) == '#') { do { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) != '\n' && str.charAt(pos) != '\r'); while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } } final ParsePosition p = new ParsePosition(pos); final Number n = fmt.parse(str, p); if (n == null) { throw new RoSi2ParseException("Texture coordinate is not a number"); } coord[i++] = n.floatValue(); pos = p.getIndex(); if (i == 2) { ts.add(new TexCoords.TexCoord(coord[0], coord[1])); i = 0; } } ts.trimToSize(); instance = new TexCoords(ts); break; } case "Triangles": case "Quads": { final LinkedList<Integer> vs = new LinkedList<>(); final String str = content.toString(); final DecimalFormat fmt = new DecimalFormat(); fmt.setParseIntegerOnly(true); int pos = 0; // parse vertex indices parsing: while (pos < str.length()) { while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) == '#') { do { pos++; if (pos == str.length()) { break parsing; } } while (str.charAt(pos) != '\n' && str.charAt(pos) != '\r'); while (Character.isWhitespace(str.charAt(pos))) { pos++; if (pos == str.length()) { break parsing; } } } final ParsePosition p = new ParsePosition(pos); final Number n = fmt.parse(str, p); if (n == null) { throw new RoSi2ParseException("Vertex index is not a number"); } vs.add(n.intValue()); pos = p.getIndex(); } instance = new PrimitiveGroup(tag.equals("Triangles") ? GL.GL_TRIANGLES : GL2.GL_QUADS, vs); break; } case "Surface": final String texturePath = getAttributeValue(varBindings, "diffuseTexture", false); final Texture texture; if (texturePath == null) { texture = null; } else { try { texture = TextureLoader.getInstance().loadTexture(gl, new File(filepath.getParentFile(), texturePath)); } catch (IOException ex) { throw new RoSi2ParseException("Texture not found: " + texturePath); } } final String shininessStr = getAttributeValue(varBindings, "shininess", false); Float shininess; if (shininessStr == null) { shininess = null; } else { try { shininess = Float.valueOf(shininessStr); if (shininess < 0.0f || shininess > 128.0f) { throw new RoSi2ParseException("Shininess value must be between 0 and 128, found: " + shininess); } } catch (final NumberFormatException e) { shininess = null; } } instance = new Surface( getColor(varBindings, "diffuseColor", true), getColor(varBindings, "ambientColor", false), getColor(varBindings, "specularColor", false), getColor(varBindings, "emissionColor", false), shininess, texture); break; default: return null; } // Store the instance if it is constant if (constant) { constantInstance = instance; } return instance; } private float[] getColor(final Map<String, String> varBindings, final String key, final boolean required) throws RoSi2ParseException { final String val = getAttributeValue(varBindings, key, required); if (val == null) { return null; } final float f1_255 = 1.0f / 255.0f; final float f1_15 = 1.0f / 15.0f; if (val.charAt(0) == '#') { // html style #rrggbb, #rgb // + self invented #rrggbbaa, #rgba switch (val.length()) { case 4: return new float[]{ f1_15 * hexDigit(val.charAt(1)), f1_15 * hexDigit(val.charAt(2)), f1_15 * hexDigit(val.charAt(3)), 1.0f }; case 5: return new float[]{ f1_15 * hexDigit(val.charAt(1)), f1_15 * hexDigit(val.charAt(2)), f1_15 * hexDigit(val.charAt(3)), f1_15 * hexDigit(val.charAt(4)) }; case 7: return new float[]{ f1_255 * ((hexDigit(val.charAt(1)) << 4) | hexDigit(val.charAt(2))), f1_255 * ((hexDigit(val.charAt(3)) << 4) | hexDigit(val.charAt(4))), f1_255 * ((hexDigit(val.charAt(5)) << 4) | hexDigit(val.charAt(6))), 1.0f }; case 9: return new float[]{ f1_255 * ((hexDigit(val.charAt(1)) << 4) | hexDigit(val.charAt(2))), f1_255 * ((hexDigit(val.charAt(3)) << 4) | hexDigit(val.charAt(4))), f1_255 * ((hexDigit(val.charAt(5)) << 4) | hexDigit(val.charAt(6))), f1_255 * ((hexDigit(val.charAt(7)) << 4) | hexDigit(val.charAt(8))) }; } } else if (val.charAt(val.length() - 1) == ')') { if (val.startsWith("rgb(")) { // css style rgb color (rgb(r,g,b) with r,g,b\in[0..255]\cup[0%,..,100%]) final String[] values = val.substring(4, val.length() - 1).replace(" ", "").split(","); if (values.length == 3) { final float[] color = new float[4]; for (int i = 0; i < 3; i++) { if (values[i].charAt(values[i].length() - 1) == '%') { color[i] = Double.valueOf(values[i].substring(0, values[i].length() - 1)).floatValue() * 0.01f; } else { color[i] = Double.valueOf(values[i]).floatValue() * f1_255; } } color[3] = 1.0f; return color; } } else if (val.startsWith("rgba(")) { // css3 style rgba color (rgba(r,g,b,a) with r,g,b\in[0..255]\cup[0%,..,100%] and a\in[0..1]) final String[] values = val.substring(5, val.length() - 1).replace(" ", "").split(","); if (values.length == 4) { final float[] color = new float[4]; for (int i = 0; i < 3; i++) { if (values[i].charAt(values[i].length() - 1) == '%') { color[i] = Double.valueOf(values[i].substring(0, values[i].length() - 1)).floatValue() * 0.01f; } else { color[i] = Double.valueOf(values[i]).floatValue() * f1_255; } } color[3] = Double.valueOf(values[3]).floatValue(); return color; } } } throw new RoSi2ParseException("invalid color format: " + val); } private static int hexDigit(final char ch) { if (ch >= '0' && ch <= '9') { return ch - '0'; } else if (ch >= 'a' && ch <= 'f') { return (ch - 'a') + 10; } throw new IllegalArgumentException(); } private float getLength(final Map<String, String> varBindings, final String key, final boolean required, final float defaultValue) throws RoSi2ParseException { float[] value = new float[1]; String[] unit = new String[1]; if (!getFloatAndUnit(varBindings, key, required, value, unit)) { return defaultValue; } if (unit[0].isEmpty() || unit[0].equals("m")) { return value[0]; } else if (unit[0].equals("mm")) { return value[0] * 0.001f; } else if (unit[0].equals("cm")) { return value[0] * 0.01f; } else if (unit[0].equals("dm")) { return value[0] * 0.1f; } else if (unit[0].equals("km")) { return value[0] * 1000.0f; } throw new RoSi2ParseException("Unexpected unit \"" + unit[0] + " (expected one of \"mm, cm, dm, m, km\")"); } private float getUnit(final Map<String, String> varBindings, final String key, final boolean required, final float defaultValue) throws RoSi2ParseException { final String unit = getAttributeValue(varBindings, key, required); if (unit == null || unit.isEmpty()) { return defaultValue; } switch (unit) { case "m": return 1.0f; case "mm": return 0.001f; case "cm": return 0.01f; case "dm": return 0.1f; case "km": return 1000.0f; } throw new RoSi2ParseException("Unexpected unit \"" + unit + " (expected one of \"mm, cm, dm, m, km\")"); } private float getAngle(final Map<String, String> varBindings, final String key, final boolean required, final float defaultValue) throws RoSi2ParseException { float[] value = new float[1]; String[] unit = new String[1]; if (!getFloatAndUnit(varBindings, key, required, value, unit)) { return defaultValue; } if (unit[0].isEmpty() || unit[0].equals("radian")) { return value[0]; } else if (unit[0].equals("degree")) { return value[0] * (float) Math.PI / 180.0f; } throw new RoSi2ParseException("Unexpected unit \"" + unit[0] + " (expected one of \"degree, radian\")"); } private boolean getFloatAndUnit(final Map<String, String> varBindings, final String key, final boolean required, final float[] value, final String[] unit) throws RoSi2ParseException { final String val = getAttributeValue(varBindings, key, required); if (val == null) { return false; } ParsePosition pos = new ParsePosition(0); final DecimalFormat fmt = new DecimalFormat(); fmt.setGroupingUsed(false); final DecimalFormatSymbols sym = (DecimalFormatSymbols) DecimalFormatSymbols.getInstance().clone(); sym.setDecimalSeparator('.'); fmt.setDecimalFormatSymbols(sym); final Number v = fmt.parse(val, pos); if (v != null) { value[0] = v.floatValue(); unit[0] = val.substring(pos.getIndex()).trim(); return true; } return false; } private String getAttributeValue(final Map<String, String> varBindings, final String key, final boolean required) throws RoSi2ParseException { final String raw = attributes.get(key); if (raw == null) { if (required) { throw new RoSi2ParseException("Missing attribute: " + key); } return null; } int varStart = raw.indexOf('$'); if (varStart < 0) { return raw; } final StringBuilder value = new StringBuilder(raw.length()); int varEnd = 0; while (varStart >= 0) { value.append(raw.substring(varEnd, varStart)); if (varStart + 1 == raw.length()) { varEnd = varStart; break; } final char c = raw.charAt(varStart + 1); final String varName; if (c == '(' || c == '{') { final char cEnd = c == '(' ? ')' : '}'; varEnd = raw.indexOf(cEnd, varStart + 2); if (varEnd < 0) { throw new RoSi2ParseException("Invalid attribute format: missing " + cEnd); } varName = raw.substring(varStart + 2, varEnd); varEnd++; } else { varEnd = varStart + 1; while (varEnd < raw.length() && Character.isLetterOrDigit(raw.charAt(varEnd))) { varEnd++; } varName = raw.substring(varStart + 1, varEnd); } final String binding = varBindings.get(varName); if (binding == null) { value.append(raw.substring(varStart, varEnd)); } else { value.append(binding); } if (varEnd == raw.length()) { break; } else { varStart = raw.indexOf('$', varEnd); } } if (varEnd < raw.length()) { value.append(raw.substring(varEnd)); } return value.toString(); } /** * Abstract base class for drawable instantiations of elements read from * ros2 files. */ public static abstract class RoSi2Drawable { private final GL2 gl; /** * Instantiated children of this element. */ protected final List<RoSi2Drawable> children; private final Rotation rotation; private final Translation translation; /** * Constructor which leaves the attributes uninitialized. Useful for * drawables containinng only data. */ protected RoSi2Drawable() { gl = null; children = new LinkedList<>(); rotation = null; translation = null; } private RoSi2Drawable(final GL2 gl, final List<RoSi2Drawable> children) throws RoSi2ParseException { this.gl = gl; this.children = children; // Find children defining a transformation Translation t = null; Rotation r = null; ListIterator<RoSi2Drawable> iter = children.listIterator(); while (iter.hasNext()) { final RoSi2Drawable cur = iter.next(); if (cur instanceof Translation) { if (t != null) { throw new RoSi2ParseException("More than one Translation element"); } iter.remove(); t = (Translation) cur; } else if (cur instanceof Rotation) { if (r != null) { throw new RoSi2ParseException("More than one Rotation element"); } iter.remove(); r = (Rotation) cur; } } translation = t; rotation = r; } /** * Draws this element and its children. */ public final void draw() { // Apply transformation if (translation != null || rotation != null) { gl.glPushMatrix(); if (translation != null) { gl.glTranslatef(translation.translation[0], translation.translation[1], translation.translation[2]); } if (rotation != null) { gl.glRotated(Math.toDegrees(rotation.rotation[0]), 1, 0, 0); gl.glRotated(Math.toDegrees(rotation.rotation[1]), 0, 1, 0); gl.glRotated(Math.toDegrees(rotation.rotation[2]), 0, 0, 1); } } // Draw this element render(gl); // Draw children for (final RoSi2Drawable child : children) { child.draw(); } // Reset transformation if (translation != null || rotation != null) { gl.glPopMatrix(); } } /** * Draws this element using the given GL object. * * @param gl GL object */ protected abstract void render(final GL2 gl); /** * Creates a display list on the given GL object which renders the * element. The display list will not be destroyed, so this object may * safely be garbage collected afterwards. * * @return number of the created display list */ public final int createDisplayList() { final int listId = gl.glGenLists(1); gl.glNewList(listId, GL2.GL_COMPILE); draw(); gl.glEndList(); return listId; } } private static class Compound extends RoSi2Drawable { public Compound(final GL2 gl, final List<RoSi2Drawable> children) throws RoSi2ParseException { super(gl, children); } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Body extends RoSi2Drawable { public Body(final GL2 gl, final List<RoSi2Drawable> children) throws RoSi2ParseException { super(gl, children); } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Translation extends RoSi2Drawable { public final float[] translation; public Translation(final float[] translation) { this.translation = translation; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Rotation extends RoSi2Drawable { public final float[] rotation; public Rotation(final float[] rotation) { this.rotation = rotation; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Appearance extends RoSi2Drawable { protected final Surface surface; public Appearance(final GL2 gl, final List<RoSi2Drawable> children) throws RoSi2ParseException { super(gl, children); Surface s = null; ListIterator<RoSi2Drawable> iter = this.children.listIterator(); while (iter.hasNext()) { final RoSi2Drawable cur = iter.next(); if (cur instanceof Surface) { if (s != null) { throw new RoSi2ParseException("More than one Surface element"); } s = (Surface) cur; iter.remove(); } } if (s == null && !getClass().equals(Appearance.class)) { throw new RoSi2ParseException(getClass().getSimpleName() + " element needs a Surface element"); } surface = s; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class BoxAppearance extends Appearance { /** * The width of the box (cy). */ private final float width; /** * The height of the box (cz). */ private final float height; /** * The depth of the box (cx). */ private final float depth; public BoxAppearance(final GL2 gl, final List<RoSi2Drawable> children, final float width, final float height, final float depth) throws RoSi2ParseException { super(gl, children); this.width = width; this.height = height; this.depth = depth; } @Override protected void render(final GL2 gl) { surface.set(gl); final float lx = depth * 0.5f; final float ly = width * 0.5f; final float lz = height * 0.5f; // -y-side gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(0, -1, 0); gl.glVertex3f(lx, -ly, -lz); gl.glVertex3f(lx, -ly, lz); gl.glVertex3f(-lx, -ly, lz); gl.glVertex3f(-lx, -ly, -lz); gl.glEnd(); // y-side gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(0, 1, 0); gl.glVertex3f(-lx, ly, lz); gl.glVertex3f(lx, ly, lz); gl.glVertex3f(lx, ly, -lz); gl.glVertex3f(-lx, ly, -lz); gl.glEnd(); // -x-side gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(-1, 0, 0); gl.glVertex3f(-lx, -ly, -lz); gl.glVertex3f(-lx, -ly, lz); gl.glVertex3f(-lx, ly, lz); gl.glVertex3f(-lx, ly, -lz); gl.glEnd(); // x-side gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(1, 0, 0); gl.glVertex3f(lx, -ly, -lz); gl.glVertex3f(lx, ly, -lz); gl.glVertex3f(lx, ly, lz); gl.glVertex3f(lx, -ly, lz); gl.glEnd(); // bottom gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(0, 0, -1); gl.glVertex3f(-lx, -ly, -lz); gl.glVertex3f(-lx, ly, -lz); gl.glVertex3f(lx, ly, -lz); gl.glVertex3f(lx, -ly, -lz); gl.glEnd(); // top gl.glBegin(GL2.GL_TRIANGLE_FAN); gl.glNormal3f(0, 0, 1); gl.glVertex3f(-lx, -ly, lz); gl.glVertex3f(lx, -ly, lz); gl.glVertex3f(lx, ly, lz); gl.glVertex3f(-lx, ly, lz); gl.glEnd(); surface.unset(gl); } } private static class SphereAppearance extends Appearance { /** * The radius of the sphere. */ private final float radius; public SphereAppearance(final GL2 gl, final List<RoSi2Drawable> children, final float radius) throws RoSi2ParseException { super(gl, children); this.radius = radius; } @Override protected void render(final GL2 gl) { final GLU glu = GLU.createGLU(gl); surface.set(gl); final GLUquadric q = glu.gluNewQuadric(); glu.gluSphere(q, radius, 16, 16); glu.gluDeleteQuadric(q); surface.unset(gl); } } private static class CylinderAppearance extends Appearance { /** * The height of the cylinder. */ private final float height; /** * The radius. */ private final float radius; public CylinderAppearance(final GL2 gl, final List<RoSi2Drawable> children, final float height, final float radius) throws RoSi2ParseException { super(gl, children); this.height = height; this.radius = radius; } @Override protected void render(final GL2 gl) { final GLU glu = GLU.createGLU(gl); surface.set(gl); final GLUquadric q = glu.gluNewQuadric(); gl.glTranslatef(0.f, 0.f, height * -0.5f); glu.gluCylinder(q, radius, radius, height, 16, 1); gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f); glu.gluDisk(q, 0, radius, 16, 1); gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f); gl.glTranslatef(0, 0, height); glu.gluDisk(q, 0, radius, 16, 1); gl.glTranslatef(0.f, 0.f, height * -0.5f); glu.gluDeleteQuadric(q); surface.unset(gl); } } private static class CapsuleAppearance extends Appearance { /** * The height of the capsule. */ private final float height; /** * The radius. */ private final float radius; public CapsuleAppearance(final GL2 gl, final List<RoSi2Drawable> children, final float height, final float radius) throws RoSi2ParseException { super(gl, children); this.height = height; this.radius = radius; } @Override protected void render(final GL2 gl) { final GLU glu = GLU.createGLU(gl); surface.set(gl); final GLUquadric q = glu.gluNewQuadric(); final float cylinderHeight = height - radius - radius; gl.glTranslatef(0.f, 0.f, cylinderHeight * -0.5f); glu.gluCylinder(q, radius, radius, cylinderHeight, 16, 1); glu.gluSphere(q, radius, 16, 16); gl.glTranslatef(0, 0, cylinderHeight); glu.gluSphere(q, radius, 16, 16); glu.gluDeleteQuadric(q); surface.unset(gl); } } private static class ComplexAppearance extends Appearance { private final Vertices vertices; private final Normals normals; private final boolean normalsDefined; private final TexCoords texCoords; private final List<PrimitiveGroup> primitiveGroups; public ComplexAppearance(final GL2 gl, final List<RoSi2Drawable> children) throws RoSi2ParseException { super(gl, children); // scan children for elements defining the complex appearance primitiveGroups = new LinkedList<>(); Vertices v = null; Normals n = null; TexCoords t = null; final ListIterator<RoSi2Drawable> iter = this.children.listIterator(); while (iter.hasNext()) { final RoSi2Drawable child = iter.next(); if (child instanceof PrimitiveGroup) { primitiveGroups.add((PrimitiveGroup) child); iter.remove(); } else if (child instanceof Vertices) { if (v != null) { throw new RoSi2ParseException("More than one Vertices element."); } v = (Vertices) child; iter.remove(); } else if (child instanceof Normals) { if (n != null) { throw new RoSi2ParseException("More than one Normals element."); } n = (Normals) child; iter.remove(); } else if (child instanceof TexCoords) { if (t != null) { throw new RoSi2ParseException("More than one TexCoords element."); } t = (TexCoords) child; iter.remove(); } } if (v == null) { throw new RoSi2ParseException("ComplexAppearance element requires a Vertices element"); } else if (primitiveGroups.isEmpty()) { throw new RoSi2ParseException("ComplexAppearance element requires a Triangles or Quads element"); } vertices = v; if (n == null) { normalsDefined = false; n = computeNormals(); } else { normalsDefined = true; } normals = n; texCoords = t; } private Normals computeNormals() { ArrayList<Normals.Normal> ns = new ArrayList<>(vertices.vertices.size()); for (Vertices.Vertex v : vertices.vertices) { ns.add(new Normals.Normal()); } for (PrimitiveGroup primitiveGroup : primitiveGroups) { ListIterator<Integer> iter = primitiveGroup.vertices.listIterator(); while (iter.hasNext()) { int i1 = iter.next(); if (i1 >= ns.size()) { iter.set(0); i1 = 0; } int i2 = iter.next(); if (i2 >= ns.size()) { iter.set(0); i2 = 0; } int i3 = iter.next(); if (i3 >= ns.size()) { iter.set(0); i3 = 0; } int i4 = 0; if (primitiveGroup.mode == GL2.GL_QUADS) { i4 = iter.next(); if (i4 >= ns.size()) { iter.set(0); } i4 = 0; } final Vertices.Vertex p1 = vertices.vertices.get(i1); final Vertices.Vertex p2 = vertices.vertices.get(i2); final Vertices.Vertex p3 = vertices.vertices.get(i3); final Vertices.Vertex u = new Vertices.Vertex(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z); final Vertices.Vertex v = new Vertices.Vertex(p3.x - p1.x, p3.y - p1.y, p3.z - p1.z); final Normals.Normal n = new Normals.Normal(u.y * v.z - u.z * v.y, u.z * v.x - u.x * v.z, u.x * v.y - u.y * v.x, 1); double len = Math.sqrt(n.x * n.x + n.y * n.y + n.z * n.z); len = len == 0 ? 1.f : 1.f / len; n.x *= len; n.y *= len; n.z *= len; ns.get(i1).add(n); ns.get(i2).add(n); ns.get(i3).add(n); if (primitiveGroup.mode == GL2.GL_QUADS) { ns.get(i4).add(n); } } } for (Normals.Normal i : ns) { if (i.length > 0) { final float mult = 1.0f / (float) i.length; i.x *= mult; i.y *= mult; i.z *= mult; } } return new Normals(ns); } @Override protected void render(final GL2 gl) { surface.set(gl, texCoords == null); for (final PrimitiveGroup primitiveGroup : primitiveGroups) { gl.glBegin(primitiveGroup.mode); final Iterator<Integer> iter = primitiveGroup.vertices.iterator(); while (iter.hasNext()) { final int i = iter.next(); if (texCoords != null && i < texCoords.coords.size()) { gl.glTexCoord2f(texCoords.coords.get(i).x, texCoords.coords.get(i).y); } if (normalsDefined) { if (iter.hasNext()) { final Normals.Normal n = normals.normals.get(iter.next()); gl.glNormal3f(n.x, n.y, n.z); } else { break; } } else { gl.glNormal3f(normals.normals.get(i).x, normals.normals.get(i).y, normals.normals.get(i).z); } gl.glVertex3f(vertices.vertices.get(i).x, vertices.vertices.get(i).y, vertices.vertices.get(i).z); } gl.glEnd(); } surface.unset(gl, texCoords == null); } } private static class Vertices extends RoSi2Drawable { public static class Vertex { public float x; public float y; public float z; public Vertex(final float x, final float y, final float z) { this.x = x; this.y = y; this.z = z; } } public final List<Vertex> vertices; public Vertices(final List<Vertex> vertices) { this.vertices = vertices; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Normals extends RoSi2Drawable { public static class Normal extends Vertices.Vertex { public int length; public Normal() { this(0, 0, 0, 0); } public Normal(final float x, final float y, final float z, final int length) { super(x, y, z); this.length = length; } /** * Addition of another normal to this one. * * @param other The other normal that will be added to this one */ void add(final Normal other) { x += other.x; y += other.y; z += other.z; length += other.length; } } public final List<Normal> normals; public Normals(final List<Normal> normals) { this.normals = normals; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class TexCoords extends RoSi2Drawable { /** * A point on a texture. */ public static class TexCoord { /** * The x-component of the point. */ public float x; /** * The y-component of the point. */ public float y; /** * Constructs a point of a texture * * @param x The x-component of the point * @param y The y-component of the point */ public TexCoord(final float x, final float y) { this.x = x; this.y = y; } }; public final List<TexCoord> coords; public TexCoords(final List<TexCoord> coords) { this.coords = coords; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class PrimitiveGroup extends RoSi2Drawable { private final int mode; private final List<Integer> vertices; public PrimitiveGroup(final int mode, final List<Integer> vertices) { this.mode = mode; this.vertices = vertices; } @Override protected void render(final GL2 gl) { // Do nothing } } private static class Surface extends RoSi2Drawable { public final float[] diffuseColor; public final float[] ambientColor; public final float[] specularColor; public final float[] emissionColor; public final float shininess; public final Texture texture; public Surface(final float[] diffuseColor, final float[] ambientColor, final float[] specularColor, final float[] emissionColor, final Float shininess, final Texture texture) { this.diffuseColor = diffuseColor; this.ambientColor = ambientColor; if (specularColor != null) { this.specularColor = specularColor; } else { this.specularColor = new float[]{0.0f, 0.0f, 0.0f, 1.0f}; } if (emissionColor != null) { this.emissionColor = emissionColor; } else { this.emissionColor = new float[]{0.0f, 0.0f, 0.0f, 1.0f}; } if (shininess != null) { this.shininess = shininess; } else { this.shininess = 0.0f; } this.texture = texture; } @Override protected void render(final GL2 gl) { // Do nothing } public void set(final GL2 gl) { set(gl, true); } public void set(final GL2 gl, final boolean defaultTextureSize) { if (ambientColor != null) { gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_DIFFUSE); gl.glMaterialfv(GL2.GL_FRONT, GL2.GL_AMBIENT, FloatBuffer.wrap(ambientColor)); } else { gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_AMBIENT_AND_DIFFUSE); } gl.glColor4fv(FloatBuffer.wrap(diffuseColor)); gl.glMaterialfv(GL2.GL_FRONT, GL2.GL_SPECULAR, FloatBuffer.wrap(specularColor)); gl.glMaterialf(GL2.GL_FRONT, GL2.GL_SHININESS, shininess); gl.glMaterialfv(GL2.GL_FRONT, GL2.GL_EMISSION, FloatBuffer.wrap(emissionColor)); if (texture != null) { gl.glBindTexture(GL2.GL_TEXTURE_2D, texture.id); if (texture.hasAlpha) { gl.glEnable(GL2.GL_BLEND); gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA); } if (defaultTextureSize) { gl.glEnable(GL2.GL_TEXTURE_GEN_S); gl.glEnable(GL2.GL_TEXTURE_GEN_T); gl.glTexGeni(GL2.GL_S, GL2.GL_TEXTURE_GEN_MODE, GL2.GL_OBJECT_LINEAR); gl.glTexGenfv(GL2.GL_S, GL2.GL_OBJECT_PLANE, FloatBuffer.wrap(new float[]{1.f, 0.f, 0.f, 0.f})); gl.glTexGeni(GL2.GL_T, GL2.GL_TEXTURE_GEN_MODE, GL2.GL_OBJECT_LINEAR); gl.glTexGenfv(GL2.GL_T, GL2.GL_OBJECT_PLANE, FloatBuffer.wrap(new float[]{0.f, 1.f, 0.f, 0.f})); } } else if (diffuseColor[3] < 1.0f) { gl.glEnable(GL2.GL_BLEND); gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA); } } public void unset(final GL2 gl) { unset(gl, true); } public void unset(final GL2 gl, final boolean defaultTextureSize) { if (texture != null) { if (defaultTextureSize) { gl.glDisable(GL2.GL_TEXTURE_GEN_S); gl.glDisable(GL2.GL_TEXTURE_GEN_T); } gl.glBindTexture(GL2.GL_TEXTURE_2D, 0); } if (diffuseColor[3] < 1.0f || (texture != null && texture.hasAlpha)) { gl.glDisable(GL2.GL_BLEND); } } } /** * Parses the given ros2 file and returns its scene element. In case no * scene element exists, the <Simulation> root element is returned. * * @param filename path to the file to parse * @return Element representing the scene * @throws teamcomm.gui.drawings.RoSi2Element.RoSi2ParseException if the * file could not be parsed as a ros2 file * @throws javax.xml.stream.XMLStreamException if the file could not be * parsed as a XML file * @throws java.io.FileNotFoundException if the file could not be found * @throws java.io.IOException on other IO errors */ public static RoSi2Element parseFile(final String filename) throws RoSi2ParseException, XMLStreamException, FileNotFoundException, IOException { // XML parser factory final XMLInputFactory factory = XMLInputFactory.newFactory(); factory.setProperty(XMLInputFactory.IS_COALESCING, true); factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false); // Stack containing the opened files final Deque<InputFileState> inputFileStack = new LinkedList<>(); // Stack containing the current element hierarchy final Deque<RoSi2Element> parentStack = new LinkedList<>(); // Map with mappings of all named elements final Map<String, RoSi2Element> namedElements = new HashMap<>(); // Flag indicating whether the parser is within the scene element boolean withinSceneElement = false; // Open the given file inputFileStack.addFirst(new InputFileState(factory, new File(filename))); // Create the root element parentStack.addFirst(new RoSi2Element(inputFileStack.getFirst().path, "Simulation", namedElements)); // Parse the file(s) while (!inputFileStack.isEmpty()) { while (inputFileStack.getFirst().reader.hasNext()) { final XMLEvent ev = inputFileStack.getFirst().reader.nextEvent(); if (ev.isStartElement()) { final StartElement e = ev.asStartElement(); final String tag = e.getName().getLocalPart(); if (tag.equals("Simulation")) { // Start actual parsing after passing Simulation element inputFileStack.getFirst().simulationTagPassed = true; } else if (tag.equals("Include")) { // Open the included file inputFileStack.addFirst(new InputFileState(factory, new File(inputFileStack.getFirst().path.getParentFile(), getXmlAttribute(e, "href", true)))); } else if (inputFileStack.getFirst().simulationTagPassed) { if (tag.equals("Set")) { // Set variable binding final String name = getXmlAttribute(e, "name", true); if (!parentStack.getFirst().vars.containsKey(name)) { parentStack.getFirst().vars.put(name, getXmlAttribute(e, "value", true)); } } else { // Create and add element final String name = getXmlAttribute(e, "name", false); @SuppressWarnings("unchecked") final RoSi2Element elem = new RoSi2Element(inputFileStack.getFirst().path, tag, name, (Iterator<Attribute>) e.getAttributes(), namedElements); if (name != null && !withinSceneElement) { namedElements.put(tag + '#' + name, elem); } if (!withinSceneElement && tag.equals("Scene")) { withinSceneElement = true; } parentStack.getFirst().children.add(elem); parentStack.addFirst(elem); } } } else if (ev.isAttribute()) { parentStack.getFirst().attributes.put(((Attribute) ev).getName().getLocalPart(), ((Attribute) ev).getValue()); } else if (ev.isCharacters()) { final Characters e = ev.asCharacters(); if (!e.isWhiteSpace()) { parentStack.getFirst().content.append(e.getData()); } } else if (ev.isEndElement()) { final String tag = ev.asEndElement().getName().getLocalPart(); if (tag.equals(parentStack.getFirst().tag) && !tag.equals("Simulation")) { if (tag.equals("Scene")) { withinSceneElement = false; } parentStack.removeFirst(); } } } // Close the current file inputFileStack.pollFirst().close(); } if (!(parentStack.size() == 1 && parentStack.getFirst().tag.equals("Simulation"))) { throw new RoSi2ParseException("File ended before parsing was complete"); } // Find the Scene element for (final RoSi2Element cur : parentStack.getFirst().children) { if (cur.tag.equals("Scene")) { return cur; } } // If no Scene element exists, return the root instead return parentStack.getFirst(); } private static String getXmlAttribute(final StartElement e, final String name, boolean required) throws RoSi2ParseException { final Attribute attr = e.getAttributeByName(new QName(name)); if (attr == null) { if (required) { throw new RoSi2ParseException("Missing attribute " + name + " on " + e.getName().getLocalPart() + " tag."); } return null; } return attr.getValue(); } /** * Exception thrown if a ros2 file could not be parsed. */ public static class RoSi2ParseException extends Exception { private static final long serialVersionUID = 439895799292899819L; private RoSi2ParseException() { } private RoSi2ParseException(final String message) { super(message); } } private static class InputFileState { public final XMLEventReader reader; public final File path; public final FileInputStream stream; public boolean simulationTagPassed; public InputFileState(final XMLInputFactory factory, final File path) throws XMLStreamException, FileNotFoundException { File p; try { p = path.getCanonicalFile(); } catch (IOException e) { p = path.getAbsoluteFile(); } this.path = p; stream = new FileInputStream(path); reader = factory.createXMLEventReader(stream); simulationTagPassed = false; } public void close() throws XMLStreamException, IOException { reader.close(); stream.close(); } } }