org.bimserver.collada.ColladaSerializer.java Source code

Java tutorial

Introduction

Here is the source code for org.bimserver.collada.ColladaSerializer.java

Source

package org.bimserver.collada;

/******************************************************************************
 * Copyright (C) 2009-2015  BIMserver.org
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *****************************************************************************/

import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.bimserver.emf.IdEObject;
import org.bimserver.emf.IfcModelInterface;
import org.bimserver.emf.PackageMetaData;
import org.bimserver.geometry.Matrix;
import org.bimserver.models.geometry.GeometryData;
import org.bimserver.models.geometry.GeometryInfo;
import org.bimserver.models.ifc2x3tc1.IfcBuildingElementProxy;
import org.bimserver.models.ifc2x3tc1.IfcColumn;
import org.bimserver.models.ifc2x3tc1.IfcCurtainWall;
import org.bimserver.models.ifc2x3tc1.IfcDoor;
import org.bimserver.models.ifc2x3tc1.IfcFeatureElementSubtraction;
import org.bimserver.models.ifc2x3tc1.IfcFlowSegment;
import org.bimserver.models.ifc2x3tc1.IfcFurnishingElement;
import org.bimserver.models.ifc2x3tc1.IfcMember;
import org.bimserver.models.ifc2x3tc1.IfcPlate;
import org.bimserver.models.ifc2x3tc1.IfcProduct;
import org.bimserver.models.ifc2x3tc1.IfcProject;
import org.bimserver.models.ifc2x3tc1.IfcRailing;
import org.bimserver.models.ifc2x3tc1.IfcRoof;
import org.bimserver.models.ifc2x3tc1.IfcSIUnit;
import org.bimserver.models.ifc2x3tc1.IfcSlab;
import org.bimserver.models.ifc2x3tc1.IfcSlabTypeEnum;
import org.bimserver.models.ifc2x3tc1.IfcSpace;
import org.bimserver.models.ifc2x3tc1.IfcStair;
import org.bimserver.models.ifc2x3tc1.IfcStairFlight;
import org.bimserver.models.ifc2x3tc1.IfcUnit;
import org.bimserver.models.ifc2x3tc1.IfcUnitAssignment;
import org.bimserver.models.ifc2x3tc1.IfcUnitEnum;
import org.bimserver.models.ifc2x3tc1.IfcWall;
import org.bimserver.models.ifc2x3tc1.IfcWallStandardCase;
import org.bimserver.models.ifc2x3tc1.IfcWindow;
import org.bimserver.models.store.SIPrefix;
import org.bimserver.plugins.PluginManager;
import org.bimserver.plugins.renderengine.RenderEngineException;
import org.bimserver.plugins.renderengine.RenderEnginePlugin;
import org.bimserver.plugins.serializers.AbstractGeometrySerializer;
import org.bimserver.plugins.serializers.ProgressReporter;
import org.bimserver.plugins.serializers.ProjectInfo;
import org.bimserver.plugins.serializers.SerializerException;
import org.bimserver.utils.UTF8PrintWriter;
import org.eclipse.emf.common.util.EList;
import org.openmali.vecmath2.Vector3d;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ColladaSerializer extends AbstractGeometrySerializer {
    private static final Logger LOGGER = LoggerFactory.getLogger(ColladaSerializer.class);
    private static final Map<Class<? extends IfcProduct>, Convertor<? extends IfcProduct>> convertors = new LinkedHashMap<Class<? extends IfcProduct>, Convertor<? extends IfcProduct>>();
    private final Map<String, Set<IfcProduct>> converted = new HashMap<String, Set<IfcProduct>>();
    private SIPrefix lengthUnitPrefix;

    private static <T extends IfcProduct> void addConvertor(Convertor<T> convertor) {
        convertors.put(convertor.getCl(), convertor);
    }

    static {
        addConvertor(new Convertor<IfcRoof>(IfcRoof.class, new double[] { 0.837255f, 0.203922f, 0.270588f }, 1.0f));
        addConvertor(new Convertor<IfcSlab>(IfcSlab.class, new double[] { 0.637255f, 0.603922f, 0.670588f }, 1.0f) {
            @Override
            public String getMaterialName(Object ifcSlab) {
                if (ifcSlab == null || !(ifcSlab instanceof IfcSlab)
                        || ((IfcSlab) ifcSlab).getPredefinedType() != IfcSlabTypeEnum.ROOF) {
                    return "IfcSlab";
                } else {
                    return "IfcRoof";
                }
            }
        });
        addConvertor(new Convertor<IfcWindow>(IfcWindow.class, new double[] { 0.2f, 0.2f, 0.8f }, 0.2f));
        addConvertor(new Convertor<IfcSpace>(IfcSpace.class, new double[] { 0.5f, 0.4f, 0.1f }, 0.2f));
        addConvertor(new Convertor<IfcDoor>(IfcDoor.class, new double[] { 0.637255f, 0.603922f, 0.670588f }, 1.0f));
        addConvertor(
                new Convertor<IfcStair>(IfcStair.class, new double[] { 0.637255f, 0.603922f, 0.670588f }, 1.0f));
        addConvertor(new Convertor<IfcStairFlight>(IfcStairFlight.class,
                new double[] { 0.637255f, 0.603922f, 0.670588f }, 1.0f));
        addConvertor(new Convertor<IfcFlowSegment>(IfcFlowSegment.class, new double[] { 0.6f, 0.4f, 0.5f }, 1.0f));
        addConvertor(new Convertor<IfcFurnishingElement>(IfcFurnishingElement.class,
                new double[] { 0.437255f, 0.603922f, 0.370588f }, 1.0f));
        addConvertor(
                new Convertor<IfcPlate>(IfcPlate.class, new double[] { 0.437255f, 0.603922f, 0.370588f }, 1.0f));
        addConvertor(
                new Convertor<IfcMember>(IfcMember.class, new double[] { 0.437255f, 0.603922f, 0.370588f }, 1.0f));
        addConvertor(new Convertor<IfcWallStandardCase>(IfcWallStandardCase.class,
                new double[] { 0.537255f, 0.337255f, 0.237255f }, 1.0f));
        addConvertor(new Convertor<IfcWall>(IfcWall.class, new double[] { 0.537255f, 0.337255f, 0.237255f }, 1.0f));
        addConvertor(new Convertor<IfcCurtainWall>(IfcCurtainWall.class, new double[] { 0.5f, 0.5f, 0.5f }, 0.5f));
        addConvertor(new Convertor<IfcRailing>(IfcRailing.class, new double[] { 0.137255f, 0.203922f, 0.270588f },
                1.0f));
        addConvertor(
                new Convertor<IfcColumn>(IfcColumn.class, new double[] { 0.437255f, 0.603922f, 0.370588f, }, 1.0f));
        addConvertor(new Convertor<IfcBuildingElementProxy>(IfcBuildingElementProxy.class,
                new double[] { 0.5f, 0.5f, 0.5f }, 1.0f));
        addConvertor(new Convertor<IfcProduct>(IfcProduct.class, new double[] { 0.5f, 0.5f, 0.5f }, 1.0f));
    }

    // Prepare a transformer for floating-point numbers into a strings, clipping extraneous zeros.
    private static final DecimalFormat decimalFormat = new DecimalFormat("#.##########");
    // Prepare a transformer for integers into strings.
    private static final DecimalFormat intFormat = new DecimalFormat("#");

    private Vector3d lowestObserved = new Vector3d();
    private Vector3d highestObserved = new Vector3d();

    @Override
    public void init(IfcModelInterface model, ProjectInfo projectInfo, PluginManager pluginManager,
            RenderEnginePlugin renderEnginePlugin, PackageMetaData packageMetaData, boolean normalizeOids)
            throws SerializerException {
        this.lengthUnitPrefix = getLengthUnitPrefix(model);
        super.init(model, projectInfo, pluginManager, renderEnginePlugin, packageMetaData, normalizeOids);
    }

    @Override
    public void reset() {
        setMode(Mode.BODY);
    }

    @Override
    public boolean write(OutputStream out, ProgressReporter progressReporter) throws SerializerException {
        if (getMode() == Mode.BODY) {
            PrintWriter writer = new UTF8PrintWriter(out);
            try {
                writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                writer.println(
                        "<COLLADA xmlns=\"http://www.collada.org/2008/03/COLLADASchema\" version=\"1.5.0\">");
                // Data sections.
                writeAssets(writer);
                writeCameras(writer);
                writeLights(writer);
                writeEffects(writer);
                writeMaterials(writer);
                writeGeometries(writer);
                writeVisualScenes(writer);
                writeScene(writer);
                // End of root section.
                writer.print("</COLLADA>");
                writer.flush();
            } catch (Exception e) {
                LOGGER.error("", e);
            }
            writer.flush();
            setMode(Mode.FINISHED);
            return true;
        } else if (getMode() == Mode.FINISHED) {
            return false;
        }
        return false;
    }

    // Provide a date formatter.
    private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");

    private void writeAssets(PrintWriter out) {
        // Produce a date based on now.
        String date = dateFormat.format(new Date());
        // Determine what a meter is and what to logically refer to that transformation as.
        Number unitMeter = (lengthUnitPrefix == null) ? 1 : Math.pow(10.0, lengthUnitPrefix.getValue());
        String unitName = (lengthUnitPrefix == null) ? "meter" : lengthUnitPrefix.name().toLowerCase();
        // Write the asset block.
        out.println(" <asset>");
        out.println("  <contributor>");
        out.println(
                "   <author>" + (getProjectInfo() == null ? "" : getProjectInfo().getAuthorName()) + "</author>");
        out.println("   <authoring_tool>BIMserver</authoring_tool>");
        out.println("   <comments>" + (getProjectInfo() == null ? "" : getProjectInfo().getDescription())
                + "</comments>");
        out.println("   <copyright>Copyright</copyright>");
        out.println("  </contributor>");
        out.println("  <created>" + date + "</created>");
        out.println("  <modified>" + date + "</modified>");
        out.println("  <unit meter=\"" + unitMeter + "\" name=\"" + unitName + "\"/>");
        out.println("  <up_axis>Z_UP</up_axis>");
        out.println(" </asset>");
    }

    private void writeGeometries(PrintWriter out) throws RenderEngineException, SerializerException {
        // Prepare the section.
        out.println(" <library_geometries>");
        // For each IfcProduct, get the geometry for each object in the product.
        Set<IfcProduct> convertedObjects = new HashSet<IfcProduct>();
        for (Class<? extends IfcProduct> cl : convertors.keySet()) {
            Convertor<? extends IfcProduct> convertor = convertors.get(cl);
            for (IfcProduct object : model.getAllWithSubTypes(cl)) {
                if (!convertedObjects.contains(object)) {
                    convertedObjects.add(object);
                    setGeometry(out, object, convertor.getMaterialName(object));
                }
            }
        }
        // Close the section.
        out.println(" </library_geometries>");
    }

    private void setGeometry(PrintWriter out, IfcProduct ifcProductObject, String material)
            throws RenderEngineException, SerializerException {
        // Mostly just skips IfcOpeningElements which one would probably not want to end up in the Collada file.
        if (ifcProductObject instanceof IfcFeatureElementSubtraction)
            return;
        //
        GeometryInfo geometryInfo = ifcProductObject.getGeometry();
        if (geometryInfo != null && geometryInfo.getTransformation() != null) {
            GeometryData geometryData = geometryInfo.getData();
            ByteBuffer indicesBuffer = ByteBuffer.wrap(geometryData.getIndices());
            indicesBuffer.order(ByteOrder.LITTLE_ENDIAN);
            // TODO: In Blender (3d modeling tool) and Three.js, normals are ignored in favor of vertex order. The incoming geometry seems to be in order 0 1 2 when it needs to be in 1 0 2. Need more test cases.
            // Failing order: (0, 1050, 2800), (0, 1050, 3100), (3580, 1050, 3100)
            // Successful order: (0, 1050, 3100), (0, 1050, 2800), (3580, 1050, 3100)
            List<Integer> list = new ArrayList<Integer>();
            while (indicesBuffer.hasRemaining())
                list.add(indicesBuffer.getInt());
            indicesBuffer.rewind();
            for (int i = 0; i < list.size(); i += 3) {
                Integer first = list.get(i);
                Integer next = list.get(i + 1);
                list.set(i, next);
                list.set(i + 1, first);
            }
            // Positions the X or the Y or the Z of (X, Y, Z).
            ByteBuffer positionsBuffer = ByteBuffer.wrap(geometryData.getVertices());
            positionsBuffer.order(ByteOrder.LITTLE_ENDIAN);
            // Do pass to find highest Z for considered objects.
            while (positionsBuffer.hasRemaining()) {
                float x = positionsBuffer.getFloat();
                float y = positionsBuffer.getFloat();
                float z = positionsBuffer.getFloat();
                // X
                if (x > highestObserved.x())
                    highestObserved.x(x);
                else if (x < lowestObserved.x())
                    lowestObserved.x(x);
                // Y
                if (y > highestObserved.y())
                    highestObserved.y(y);
                else if (y < lowestObserved.y())
                    lowestObserved.y(y);
                // Z
                if (z > highestObserved.z())
                    highestObserved.z(z);
                else if (z < lowestObserved.z())
                    lowestObserved.z(z);
            }
            positionsBuffer.rewind();
            //
            ByteBuffer normalsBuffer = ByteBuffer.wrap(geometryData.getNormals());
            normalsBuffer.order(ByteOrder.LITTLE_ENDIAN);
            // Create a geometry identification number in the form of: geom-320450
            long oid = ifcProductObject.getOid();
            String id = String.format("geom-%d", oid);
            // If the material doesn't exist in the converted map, add it.
            if (!converted.containsKey(material))
                converted.put(material, new HashSet<IfcProduct>());
            // Add the current IfcProduct to the appropriate entry in the material map.
            converted.get(material).add(ifcProductObject);
            // Name for geometry.
            String name = (ifcProductObject.getGlobalId() == null) ? "[NO_GUID]" : ifcProductObject.getGlobalId();
            // Counts.
            int vertexComponentsTotal = positionsBuffer.capacity() / 4,
                    normalComponentsTotal = normalsBuffer.capacity() / 4;
            int verticesCount = positionsBuffer.capacity() / 12, normalsCount = normalsBuffer.capacity() / 12,
                    triangleCount = indicesBuffer.capacity() / 12;
            // Vertex scalars as one long string: 4.05 2 1 55.0 34.01 2
            String stringPositionScalars = byteBufferToFloatingPointSpaceDelimitedString(positionsBuffer);
            // Normal scalars as one long string: 4.05 2 1 55.0 34.01 2
            String stringNormalScalars = byteBufferToFloatingPointSpaceDelimitedString(normalsBuffer); //doubleBufferToFloatingPointSpaceDelimitedString(flippedNormalsBuffer);
            // Vertex indices as one long string: 1 0 2 0 3 2 5 4 6
            String stringIndexScalars = listToSpaceDelimitedString(list, intFormat);
            // Write geometry block for this IfcProduct (i.e. IfcRoof, IfcSlab, etc).
            out.println(" <geometry id=\"" + id + "\" name=\"" + name + "\">");
            out.println("  <mesh>");
            out.println("   <source id=\"positions-" + oid + "\" name=\"positions-" + oid + "\">");
            out.println("    <float_array id=\"positions-array-" + oid + "\" count=\"" + vertexComponentsTotal
                    + "\">" + stringPositionScalars + "</float_array>");
            out.println("    <technique_common>");
            out.println("     <accessor count=\"" + verticesCount + "\" offset=\"0\" source=\"#positions-array-"
                    + oid + "\" stride=\"3\">");
            out.println("      <param name=\"X\" type=\"float\"></param>");
            out.println("      <param name=\"Y\" type=\"float\"></param>");
            out.println("      <param name=\"Z\" type=\"float\"></param>");
            out.println("     </accessor>");
            out.println("    </technique_common>");
            out.println("   </source>");
            out.println("   <source id=\"normals-" + oid + "\" name=\"normals-" + oid + "\">");
            out.println("    <float_array id=\"normals-array-" + oid + "\" count=\"" + normalComponentsTotal + "\">"
                    + stringNormalScalars + "</float_array>");
            out.println("    <technique_common>");
            out.println("     <accessor count=\"" + normalsCount + "\" offset=\"0\" source=\"#normals-array-" + oid
                    + "\" stride=\"3\">");
            out.println("      <param name=\"X\" type=\"float\"></param>");
            out.println("      <param name=\"Y\" type=\"float\"></param>");
            out.println("      <param name=\"Z\" type=\"float\"></param>");
            out.println("     </accessor>");
            out.println("    </technique_common>");
            out.println("   </source>");
            out.println("   <vertices id=\"vertices-" + oid + "\">");
            out.println("    <input semantic=\"POSITION\" source=\"#positions-" + oid + "\"/>");
            out.println("    <input semantic=\"NORMAL\" source=\"#normals-" + oid + "\"/>");
            out.println("   </vertices>");
            out.println("   <triangles count=\"" + triangleCount + "\" material=\"Material-" + oid + "\">");
            out.println("    <input offset=\"0\" semantic=\"VERTEX\" source=\"#vertices-" + oid + "\"/>");
            out.println("    <p>" + stringIndexScalars + "</p>");
            out.println("   </triangles>");
            out.println("  </mesh>");
            out.println(" </geometry>");
        }
    }

    @SuppressWarnings("unused")
    private String doubleBufferToFloatingPointSpaceDelimitedString(DoubleBuffer buffer) {
        // For each scalar in the buffer, turn it into a string, adding it to the overall list.
        List<String> stringScalars = doubleBufferToStringList(buffer, decimalFormat);
        // Send back a space-delimited list of the strings: 1 2.45 0
        return StringUtils.join(stringScalars, " ");
    }

    private String byteBufferToFloatingPointSpaceDelimitedString(ByteBuffer buffer) {
        // For each scalar in the buffer, turn it into a string, adding it to the overall list.
        List<String> stringScalars = floatBufferToStringList(buffer, decimalFormat);
        // Send back a space-delimited list of the strings: 1 2.45 0
        return StringUtils.join(stringScalars, " ");
    }

    @SuppressWarnings("unused")
    private String byteBufferToIntPointSpaceDelimitedString(ByteBuffer buffer) {
        // Prepare to store integers as a list of strings.
        List<String> stringScalars = intBufferToStringList(buffer, intFormat);
        // Send back a space-delimited list of the strings: 1 2 0
        return StringUtils.join(stringScalars, " ");
    }

    private List<String> doubleBufferToStringList(DoubleBuffer buffer, Format formatter) {
        // Transform the array into a list.
        List<Float> list = new ArrayList<Float>();
        while (buffer.hasRemaining())
            list.add(new Float(buffer.get()));
        // Get the data as a list of String objects.
        return listToStringList(list, formatter);
    }

    private List<String> floatBufferToStringList(ByteBuffer buffer, Format formatter) {
        // Transform the array into a list.
        List<Float> list = new ArrayList<Float>();
        while (buffer.hasRemaining())
            list.add(new Float(buffer.getFloat()));
        // Get the data as a list of String objects.
        return listToStringList(list, formatter);
    }

    private List<String> intBufferToStringList(ByteBuffer buffer, Format formatter) {
        List<Integer> list = new ArrayList<Integer>();
        while (buffer.hasRemaining())
            list.add(new Integer(buffer.getInt()));
        // Get the data as a list of String objects.
        return listToStringList(list, formatter);
    }

    private String floatArrayToSpaceDelimitedString(float[] matrix) {
        List<Float> floatMatrix = floatArrayToFloatList(matrix);
        List<String> list = listToStringList(floatMatrix, decimalFormat);
        // Get data as space-delimited string: 1.004 5.0 24.00145
        return StringUtils.join(list, " ");
    }

    private List<Float> floatArrayToFloatList(float[] array) {
        List<Float> list = new ArrayList<Float>();
        for (float f : array)
            list.add(new Float(f));
        return list;
    }

    private String listToSpaceDelimitedString(List<?> list, Format formatter) {
        List<String> stringScalars = listToStringList(list, formatter);
        return StringUtils.join(stringScalars, " ");
    }

    private List<String> listToStringList(List<?> list, Format formatter) {
        // Prepare to store floating-points as a list of strings.
        List<String> stringScalars = new ArrayList<String>();
        // For each scalar in the buffer, turn it into a string, adding it to the overall list.
        for (Object scalar : list) {
            String scalarAsString = formatter.format(scalar);
            stringScalars.add(scalarAsString);
        }
        return stringScalars;
    }

    private void writeScene(PrintWriter out) {
        out.println(" <scene>");
        out.println("  <instance_visual_scene url=\"#VisualSceneNode\"/>");
        out.println(" </scene>");
    }

    //String leftLightLocationString = String.format("%f %f %f", leftLightLocation.x(), leftLightLocation.y(), leftLightLocation.z());
    @SuppressWarnings("unused")
    private void writeVisualScenes(PrintWriter out) {
        // Open the section.
        out.println(" <library_visual_scenes>");
        out.println("  <visual_scene id=\"VisualSceneNode\" name=\"VisualSceneNode\">");
        // Write each IFC object as a node entry (maps to a displayed object).
        for (String material : converted.keySet()) {
            Set<IfcProduct> ids = converted.get(material);
            for (IfcProduct product : ids) {
                GeometryInfo geometryInfo = product.getGeometry();
                if (geometryInfo != null && geometryInfo.getTransformation() != null) {
                    out.println("   <node id=\"node-" + product.getOid() + "\" name=\"node-" + product.getOid()
                            + "\">");
                    printMatrix(out, geometryInfo);
                    out.println("    <instance_geometry url=\"#geom-" + product.getOid() + "\">");
                    out.println("     <bind_material>");
                    out.println("      <technique_common>");
                    out.println("       <instance_material symbol=\"Material-" + product.getOid() + "\" target=\"#"
                            + material + "Material\"/>");
                    out.println("      </technique_common>");
                    out.println("     </bind_material>");
                    out.println("    </instance_geometry>");
                    out.println("   </node>");
                }
            }
        }
        // Create convenience variables to simplify the perceived complexity of the equations.
        float lx = (float) lowestObserved.x(), ly = (float) lowestObserved.y(), lz = (float) lowestObserved.z();
        float hx = (float) highestObserved.x(), hy = (float) highestObserved.y(), hz = (float) highestObserved.z();
        // Derive useful information from the observed boundary of the IFC objects.
        Vector3d delta = new Vector3d(hx, hy, hz);
        delta.sub(lowestObserved);
        // Move the light left (-x) and back (-y) and up (+z) at 20% of the size of the observed objects. 
        Vector3d leftLightLocation = new Vector3d(lx - (0.2 * delta.x()), ly - (0.2 * delta.y()),
                hz + (0.2 * delta.z()));
        float x = (float) leftLightLocation.x(), y = (float) leftLightLocation.y(),
                z = (float) leftLightLocation.z();
        // Move left (-x) and back (-y) at 500% and up (+z) at 200% of the light (so that the objects are in sensing range of the camera). 
        Vector3d leftCameraLocation = new Vector3d(5 * x, 5 * y, 2 * z);
        float cx = (float) leftCameraLocation.x(), cy = (float) leftCameraLocation.y(),
                cz = (float) leftCameraLocation.z();
        // TODO: Three.js doesn't seem to care about the camera and the light.
        // Include the camera.
        out.println("   <node id=\"Camera\" name=\"Camera\">");
        out.println("    <translate>" + cx + " " + cy + " " + cz + "</translate>");
        out.println("    <rotate>" + 0f + " " + 0f + " " + 1f + " " + -45f + "</rotate>");
        out.println("    <rotate>" + 1f + " " + 0f + " " + 0f + " " + 45f + "</rotate>");
        out.println("    <instance_camera url=\"#PerspCamera\"/>");
        out.println("   </node>");
        // Include the light.
        out.println("   <node id=\"Light\" name=\"Light\">");
        out.println("    <translate>" + x + " " + y + " " + z + "</translate>");
        out.println("    <rotate>" + 0f + " " + 0f + " " + 1f + " " + 225f + "</rotate>");
        out.println("    <rotate>" + 0f + " " + 1f + " " + 0f + " " + 45f + "</rotate>");
        out.println("    <instance_light url=\"#light-lib\"/>");
        out.println("   </node>");
        // Close the section.
        out.println("  </visual_scene>");
        out.println(" </library_visual_scenes>");
    }

    private void printMatrix(PrintWriter out, GeometryInfo geometryInfo) {
        ByteBuffer transformation = ByteBuffer.wrap(geometryInfo.getTransformation());
        transformation.order(ByteOrder.LITTLE_ENDIAN);
        FloatBuffer floatBuffer = transformation.asFloatBuffer();
        // Prepare to create the transform matrix.
        float[] matrix = new float[16];
        // Add the first 16 values of the buffer.
        for (int i = 0; i < matrix.length; i++)
            matrix[i] = floatBuffer.get();
        // Switch from column-major (x.x ... x.y ... x.z ... 0 ...) to row-major orientation (x.x x.y x.z 0 ...)?
        matrix = Matrix.changeOrientation(matrix);
        // List all 16 elements of the matrix as a single space-delimited String object.
        out.println("    <matrix>" + floatArrayToSpaceDelimitedString(matrix) + "</matrix>");
    }

    private void writeEffects(PrintWriter out) {
        out.println(" <library_effects>");
        for (Convertor<? extends IfcProduct> convertor : convertors.values())
            writeEffect(out, convertor.getMaterialName(null), convertor.getColors(), convertor.getOpacity());
        out.println(" </library_effects>");
    }

    private void writeEffect(PrintWriter out, String name, double[] colors, double transparency) {
        out.println("  <effect id=\"" + name + "-fx\">");
        out.println("   <profile_COMMON>");
        out.println("    <technique sid=\"common\">");
        out.println("     <phong>");
        out.println("      <emission>");
        out.println("       <color>0 0 0 1</color>");
        out.println("      </emission>");
        out.println("      <ambient>");
        out.println("       <color>0 0 0 1</color>");
        out.println("      </ambient>");
        out.println("      <diffuse>");
        out.println(
                "       <color>" + colors[0] + " " + colors[1] + " " + colors[2] + " " + transparency + "</color>");
        out.println("      </diffuse>");
        out.println("      <specular>");
        out.println("       <color>0.5 0.5 0.5 1</color>");
        out.println("      </specular>");
        out.println("      <shininess>");
        out.println("       <float>16</float>");
        out.println("      </shininess>");
        out.println("      <reflective>");
        out.println("       <color>0 0 0 1</color>");
        out.println("      </reflective>");
        out.println("      <reflectivity>");
        out.println("       <float>0.5</float>");
        out.println("      </reflectivity>");
        out.println("      <index_of_refraction>");
        out.println("       <float>0</float>");
        out.println("      </index_of_refraction>");
        out.println("     </phong>");
        out.println("    </technique>");
        out.println("   </profile_COMMON>");
        out.println("  </effect>");
    }

    private void writeLights(PrintWriter out) {
        // TODO: Lights defined in library_lights of COLLADA file must be used in some visual_scene to prevent crashes with collada2gltf (stand-alone OpenGL Transmission Format exporter).
        out.println(" <library_lights>");
        out.println("  <light id=\"light-lib\" name=\"light\">");
        out.println("   <technique_common>");
        out.println("    <directional>");
        out.println("     <color>1 1 1</color>");
        out.println("    </directional>");
        out.println("   </technique_common>");
        out.println("   <technique profile=\"MAX3D\">");
        out.println("    <intensity>1.000000</intensity>");
        out.println("   </technique>");
        out.println("  </light>");
        out.println(" </library_lights>");
    }

    private void writeCameras(PrintWriter out) {
        out.println(" <library_cameras>");
        out.println("  <camera id=\"PerspCamera\" name=\"PerspCamera\">");
        out.println("   <optics>");
        out.println("    <technique_common>");
        out.println("     <perspective>");
        out.println("      <yfov>37.8493</yfov>");
        out.println("      <aspect_ratio>1</aspect_ratio>");
        out.println("      <znear>10</znear>");
        out.println("      <zfar>1000</zfar>");
        out.println("     </perspective>");
        out.println("    </technique_common>");
        out.println("   </optics>");
        out.println("  </camera>");
        out.println("  <camera id=\"testCameraShape\" name=\"testCameraShape\">");
        out.println("   <optics>");
        out.println("    <technique_common>");
        out.println("     <perspective>");
        out.println("      <yfov>37.8501</yfov>");
        out.println("      <aspect_ratio>1</aspect_ratio>");
        out.println("      <znear>0.01</znear>");
        out.println("      <zfar>1000</zfar>");
        out.println("     </perspective>");
        out.println("    </technique_common>");
        out.println("   </optics>");
        out.println("  </camera>");
        out.println(" </library_cameras>");
    }

    private void writeMaterials(PrintWriter out) {
        out.println(" <library_materials>");
        for (Convertor<? extends IfcProduct> convertor : convertors.values())
            writeMaterial(out, convertor.getMaterialName(null));
        out.println(" </library_materials>");
    }

    private void writeMaterial(PrintWriter out, String materialName) {
        out.println("  <material id=\"" + materialName + "Material\" name=\"" + materialName + "Material\">");
        out.println("   <instance_effect url=\"#" + materialName + "-fx\"/>");
        out.println("  </material>");
    }

    private static SIPrefix getLengthUnitPrefix(IfcModelInterface model) {
        SIPrefix lengthUnitPrefix = null;
        boolean prefixFound = false;
        Map<Long, IdEObject> objects = model.getObjects();
        for (IdEObject object : objects.values()) {
            if (object instanceof IfcProject) {
                IfcUnitAssignment unitsInContext = ((IfcProject) object).getUnitsInContext();
                if (unitsInContext != null) {
                    EList<IfcUnit> units = unitsInContext.getUnits();
                    for (IfcUnit unit : units) {
                        if (unit instanceof IfcSIUnit) {
                            IfcSIUnit ifcSIUnit = (IfcSIUnit) unit;
                            IfcUnitEnum unitType = ifcSIUnit.getUnitType();
                            if (unitType == IfcUnitEnum.LENGTHUNIT) {
                                prefixFound = true;
                                switch (ifcSIUnit.getPrefix()) {
                                case EXA:
                                    lengthUnitPrefix = SIPrefix.EXAMETER;
                                    break;
                                case PETA:
                                    lengthUnitPrefix = SIPrefix.PETAMETER;
                                    break;
                                case TERA:
                                    lengthUnitPrefix = SIPrefix.TERAMETER;
                                    break;
                                case GIGA:
                                    lengthUnitPrefix = SIPrefix.GIGAMETER;
                                    break;
                                case MEGA:
                                    lengthUnitPrefix = SIPrefix.MEGAMETER;
                                    break;
                                case KILO:
                                    lengthUnitPrefix = SIPrefix.KILOMETER;
                                    break;
                                case HECTO:
                                    lengthUnitPrefix = SIPrefix.HECTOMETER;
                                    break;
                                case DECA:
                                    lengthUnitPrefix = SIPrefix.DECAMETER;
                                    break;
                                case DECI:
                                    lengthUnitPrefix = SIPrefix.DECIMETER;
                                    break;
                                case CENTI:
                                    lengthUnitPrefix = SIPrefix.CENTIMETER;
                                    break;
                                case MILLI:
                                    lengthUnitPrefix = SIPrefix.MILLIMETER;
                                    break;
                                case MICRO:
                                    lengthUnitPrefix = SIPrefix.MICROMETER;
                                    break;
                                case NANO:
                                    lengthUnitPrefix = SIPrefix.NANOMETER;
                                    break;
                                case PICO:
                                    lengthUnitPrefix = SIPrefix.PICOMETER;
                                    break;
                                case FEMTO:
                                    lengthUnitPrefix = SIPrefix.FEMTOMETER;
                                    break;
                                case ATTO:
                                    lengthUnitPrefix = SIPrefix.ATTOMETER;
                                    break;
                                case NULL:
                                    lengthUnitPrefix = SIPrefix.METER;
                                    break;
                                }
                                break;
                            }
                        }
                    }
                }
            }
            if (prefixFound)
                break;
        }
        return lengthUnitPrefix;
    }
}