Homework 2 Notes

1. Instructions

This homework is about persistence and serialization. We will first finish building the vector graphics library from Homework 1. Then we will add the ability to save and load a Scene object to and from an XML file.

1.1. Finishing Vector Graphics Library

In addition to the Point and VectorGraphic clases from Homework 1, we will add the following classes under the VG namespace:

  • Scene: A collection of Layers. Also has width and height properties.
  • Layer: A collection of PlacedVectorGraphics. Also has a name.
  • PlacedVectorGraphic: A VectorGraphic with a Point for its origin.

1.2 Converting between XML file and DOM

To enable external persistence, we will implement the following classes under the Xml namespace:

  • XML Element classes:
    • TinyElement as concrete implementation using TinyXML2.
    • IElement interface and ElementFactory to hide the implementation details in case we want to switch to another XML library in the future.
  • XmlReader: reads an XML file into an IElement DOM object.
  • XMLWriter: writes an IElement DOM object to an XML file.

1.3 Converting between DOM and Scene object

And finally, we will implement the following classes under the VG namespace:

  • SceneReader: Deserialize an IElement DOM object (that is already built by XmlReader) to create a Scene object.
  • SceneWriter: Serialize a Scene object to an IElement DOM object (that can be written later by XmlWriter).

1.4 Summary

As illustrated below, the XML classes above (1.2 and 1.3) are used to convert between XML file <-> DOM <-> Scene hierarchy:

hw2_xml_diagram

Here is the simplified UML diagram for the vector graphics library:

hw2_uml


2. TinyXML2

2.1 Introduction

TinyXML2 is a lightweight, open-source XML parser for C++. It is a single header file and a single source file. It is not a full-featured XML parser, but it is easy to use and has a permissive license. We will use it to serialize and deserialize objects.

However, note that XML is a text-based markup language, and when you serialize an object to XML, you're essentially converting it into a string representation. The type information may or may not be preserved based on how you handle the serialization and deserialization.

I have created a separate page on TinyXML2 with more details.


3. VG::Scene Class

3.1 Introduction

Scene is a collection of Layers. The assignment specifies that it should have the followings:

  • Appropriate constructors.
  • Insert, remove, iteration support.
  • Width and height properties with accessors.

3.2 Design Considerations

  • Which Layer is on top?: Traditionally, a way to resolve this is to use z-ordering. However, this is not a good solution because it requires the user to keep track of the z-ordering of the layers. A more intuitive depth control for the user is relative ordering. We simply use a linear data structure to determine the order of drawing.

  • Choosing the right data structure: We have three choices: std::vector, std::list, and std::deque. Using std::list for managing Layers within a Scene object makes a lot of sense for several reasons. The linked-list nature of std::list is great for efficient insertions, deletions, and rearrangements, all at O(1) complexity. This is especially helpful if you're frequently updating the Layers. The list's non-contiguous memory allocation offers additional flexibility, as you don't have to worry about resizing and reallocation like you would with a vector. One key advantage is iterator stability; once you have an iterator pointing to an element, it stays valid even if other elements are inserted or removed. This makes it a lot easier and safer to manipulate your collection. Plus, std::list comes with some pretty handy member functions like splice, merge, sort, reverse, and unique, which can be particularly useful for specific use-cases. The linear access model isn't a drawback for this scenario since the main use-case is likely iterating through the entire list of Layers, rather than needing random access.

3.3 Implementation

Take a look at Scene.h:

#pragma once

#include <list>

#include "Layer.h"

namespace VG
{

    class Scene
    {
    private:
        using LayerCollection = std::list<Layer>;

    public:
        Scene(int width, int height);

        Scene() = default;
        Scene(const Scene &other) = default;
        Scene& operator=(const Scene &other) = default;
        Scene(Scene &&other) = default;
        Scene& operator=(Scene &&other) = default;
        ~Scene() = default;

        using LayerIterator = LayerCollection::const_iterator;

        void insertLayer(const Layer &layer);
        void removeLayer(const Layer &layer);

        LayerIterator begin() const;
        LayerIterator end() const;

        int getWidth() const;
        int getHeight() const;

        void setWidth(int width);
        void setHeight(int height);

    private:
        LayerCollection layers;
        int width = 0;
        int height = 0;
    };

}

Note that LayerIterator is an alias for std::list<Layer>::const_iterator. Based on our knowledge about std::list, we know that this is a constant bidirectional iterator. This means that we can traverse the list in both directions, and we can modify the elements in the list. However, we cannot modify the list itself. For example, we can't insert or remove elements from the list.

Scene::LayerIterator Scene::begin() const
{
    return layers.begin();
}

Scene::LayerIterator Scene::end() const
{
    return layers.end();
}

The function itself is marked as const, which means that within this function, the this pointer and all its members are treated as const. This implies that layers is treated as a const std::list<Layer> within this function. As a result, the layers.begin() invocation within this function returns a std::list<Layer>::const_iterator, rather than a std::list<Layer>::iterator.


4. VG::Layer Class

4.1 Introduction

Layer is a collection of PlacedVectorGraphics. It also has a name.

4.2 Design Considerations

  • Future extensibility: We want to be able to add more properties to Layer in the future. For example, allow the user to lock or hide a layer.

  • Which PlacedVectorGraphic is on top?: We need to be able to determine which PlacedVectorGraphic is on top. This is important for drawing and hit testing. We can do this by using a std::vector to store the PlacedVectorGraphics, and then use std::sort to sort them by their depth property. This is a good example of how we can use the standard library to simplify our code.

4.3 Implementation

Take a look at Layer.h:

#pragma once

#include <list>
#include <string>

#include "PlacedGraphic.h"

namespace VG
{

    class Layer
    {
    private:
        using PlacedGraphicCollection = std::list<PlacedGraphic>;

    public:
        Layer(const std::string &alias = "");

        Layer(const Layer &other) = default;
        Layer &operator=(const Layer &other) = default;
        Layer(Layer &&other) = default;
        Layer &operator=(Layer &&other) = default;
        ~Layer() = default;

        using PlacedGraphicIterator = PlacedGraphicCollection::const_iterator;

        void insertGraphic(const PlacedGraphic &graphic);
        void removeGraphic(const PlacedGraphic& placedGraphic);

        PlacedGraphicIterator begin() const;
        PlacedGraphicIterator end() const;

        std::string getAlias() const;
        void setAlias(const std::string &alias);

        bool operator==(const Layer& rhs) const;
        bool operator!=(const Layer& rhs) const;

    private:
        PlacedGraphicCollection graphics;
        std::string alias;
    };

}

Similar to Scene, we also added iterator support to Layer.


5. VG::PlacedVectorGraphic Class

5.1 Introduction

PlacedVectorGraphic is a VectorGraphic with a Point for its origin. It is designed this way so that VectorGraphic can have local coordinates, and PlacedVectorGraphic can have world coordinates.

5.2 Design Considerations

  • PlacedVectorGraphic should shared ownership of the VectorGraphic: This is because we want to be able to share the same VectorGraphic object across multiple PlacedVectorGraphic objects. To make it clear, we use the H prefix to indicate that this is a handle to a VectorGraphic object.
  • friend method for comparing equality: We need to be able to compare PlacedVectorGraphic objects for equality. However, we can't do this by overloading the == operator because we don't have access to the private members of PlacedVectorGraphic. Instead, we can use a friend method to compare the private members of two PlacedVectorGraphic objects.

5.3 Implementation

Take a look at PlacedGraphic.h:

#pragma once

#include "Point.h"
#include "VectorGraphic.h"

#include <memory>

namespace VG
{

    using HVectorGraphic = std::shared_ptr<VectorGraphic>;

    class PlacedGraphic
    {
    public:
        PlacedGraphic();
        PlacedGraphic(const Point &placement, const HVectorGraphic &graphic);

        PlacedGraphic(const PlacedGraphic &other) = default;
        PlacedGraphic &operator=(const PlacedGraphic &other) = default;
        PlacedGraphic(Point &&placement, HVectorGraphic &&graphic);
        PlacedGraphic &operator=(PlacedGraphic &&other) = default;
        ~PlacedGraphic() = default;

        void setPlacementPoint(Point const &placement);
        const Point &getPlacementPoint() const;

        void setGraphic(HVectorGraphic const &graphic);
        const HVectorGraphic &getGraphic() const;

    private:
        friend bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);

        Point placementPoint;
        HVectorGraphic graphic;
    };

    bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
    bool operator!=(const PlacedGraphic &lhs, const PlacedGraphic &rhs);

}

Note that we have two operator== functions:

  1. Inside the class:
    friend bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
    This line declares the operator== function as a friend of the PlacedGraphic class, granting it access to the class's private members. This is not the function declaration; it's just informing the class about who its friends are. Also, it does not matter if this is done in the public or private section of the class. The friend keyword is not an access specifier. It's just a way to grant access privileges to a function.

  2. Outside the class:
    bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
    This is the actual declaration of the operator== function. It lets the rest of the code (or any other code that includes this header) know that such a function exists. When this function is implemented (either in the same file or in a corresponding source file), it will have access to the private members of PlacedGraphic due to the friend declaration inside the class.

  3. operator!=:
    The operator!= function is also declared outside the class. Typically, it's implemented in terms of the operator== function, like:
    return !(lhs == rhs);
    There's no need to make this function a friend (unless it requires access to private members, which is unlikely if it's just using operator==).

In summary, the friend keyword inside the class grants access privileges, but it does not serve as the function's declaration for linking purposes. The actual declaration outside the class is what's used by the linker and other parts of the code to know that the function exists.


6. XML Element Classes

6.1 Introduction

XML format is made up of elements arranged in a tree structure. Each element has a name, attributes, and child elements. For our vector graphics library, we might want to represent a Scene with the following XML:

<Scene width="800" height="600">
    <Layer alias="sky">
        <PlacedGraphic x="0" y="0">
            <VectorGraphic closed="true">
                <Point x="0" y="10" />
                <!-- etc... -->
            </VectorGraphic>
        </PlacedGraphic>
        <PlacedGraphic x="700" y="0">
            <VectorGraphic closed="true">
                <!-- etc... -->
            </VectorGraphic>
        </PlacedGraphic>
    </Layer>
    <Layer alias="mountains">
        <PlacedGraphic x="0" y="0">
            <VectorGraphic closed="false">
                <!-- etc... -->
            </VectorGraphic>
        </PlacedGraphic>
    </Layer>
    <Layer alias="houses">
        <!-- etc... -->
    </Layer>
</Scene>

For something this tangible and commonplace, we should define a class for it, making it a first-class concept (see lecture 2) of our library. This Elememt class must support the following operations:

  • Constructing an element from an XML string.
  • Getting and setting attributes.
  • Getting existing child elements.
  • Adding new child elements.

6.2 Design Considerations

  • IElement interface: We want to be able to use different XML libraries in the future. For example, we might want to use TinyXML2 for now, but switch to another library later. To make this possible, we need to define an interface for XML elements. This is the IElement interface. It has methods for getting and setting attributes, getting child elements, etc. We will implement this interface using TinyXML2 in the TinyElement class.

6.3 Implementation

Here is how we define the IElement interface, making sure it supports all the operations we need:

#pragma once

#include <memory>
#include <string>
#include <vector>
#include <map>

namespace Xml
{
    class IElement;
    using HElement = std::shared_ptr<IElement>;
    using ElementCollection = std::vector<HElement>;
    using AttributeMap = std::map<std::string, std::string>;

    class IElement
    {
    public:
        IElement() = default;

        IElement(const IElement &) = default;
        IElement &operator=(const IElement &) = default;
        IElement(IElement &&) = default;
        IElement &operator=(IElement &&) = default;
        virtual ~IElement() = default;

        virtual void createFromXml(std::string &xmlStr) = 0;

        virtual std::string getName() const noexcept = 0;

        virtual void setAttribute(const std::string &name, const std::string &value) = 0;
        virtual std::string getAttribute(const std::string &name) const = 0;
        virtual AttributeMap getAttributes() const noexcept = 0;

        virtual ElementCollection getChildElements() const noexcept = 0;

        virtual HElement appendChild(const std::string &name) noexcept = 0;
    };

    // IXmlReader also defined here (not shown).
}

Note that since this interface serves as an API to the rest of the code, it's a good idea to mark methods with noexcept if they don't throw exceptions. It provides clear contracts to the users of the interface about what to expect regarding exception handling. If a function marked with noexcept does end up throwing an exception, then the program will call std::terminate() to end the program. This ensures that the noexcept guarantee is strictly enforced at runtime.

Now we can implement the TinyElement class using TinyXML2:

#pragma once

#include "XmlInterfaces.h"
#include "../tinyxml2/tinyxml2.h"

namespace Xml
{

    class TinyElement : public IElement
    {
    public:
        TinyElement();
        TinyElement(const std::string &name);

        TinyElement(const TinyElement &other) = delete;
        TinyElement(TinyElement &&other) = delete;
        virtual ~TinyElement() = default;

        TinyElement &operator=(const TinyElement &other) = delete;
        TinyElement &operator=(TinyElement &&other) = delete;

        void createFromXml(std::string &xmlStr) override;

        std::string getName() const noexcept override;

        void setAttribute(const std::string &name, const std::string &value) override;
        std::string getAttribute(const std::string &name) const override;
        AttributeMap getAttributes() const noexcept override;

        ElementCollection getChildElements() const noexcept override;

        HElement appendChild(const std::string &name) noexcept override;

    private:
        TinyElement(tinyxml2::XMLElement *node);

        std::shared_ptr<tinyxml2::XMLDocument> myDocument;
        tinyxml2::XMLElement *myElement;
    };

}

Here, we are mainly just overriding the virtual functions from the IElement interface. Notice that we delete the copy and move constructors and assignment operators. Because each TinyElement object owns a tinyxml2::XMLElement object, copying them could be error-prone and confusing. So we just delete them to prevent accidental copies.

We also added 3 private members:

  • TinyElement(tinyxml2::XMLElement *node);: This is a private constructor that takes a pointer to a tinyxml2::XMLElement. This might be used internally by the class to create a TinyElement directly from an existing tinyxml2 element, without exposing this capability to external users. It’s a way to encapsulate the usage of tinyxml2 and keep it internal to the implementation.

  • std::shared_ptr<tinyxml2::XMLDocument> myDocument;: tinyxml2::XMLDocument represents an entire XML document. Storing a shared pointer to it ensures that the XML document stays alive as long as any associated TinyElement objects exist. It uses shared_ptr to manage the ownership and lifetime of the XMLDocument.

  • tinyxml2::XMLElement *myElement;: This is a direct pointer to the specific XML element that this TinyElement instance wraps. It provides direct access to the underlying tinyxml2 representation of the element.

Under the hood, TinyElement uses tinyxml2 to parse the XML string and create a DOM object. Here is the implementation of createFromXml:

void TinyElement::createFromXml(std::string &xmlStr)
{
    if (myDocument->Parse(xmlStr.c_str()) != XML_SUCCESS)
    {
        throw std::runtime_error("Error Parsing XML stream");
    }

    myElement = myDocument->RootElement();
}

However, just using IElement interface alone is not enough. We need a way to create IElement objects. This is where the ElementFactory class comes in:

#pragma once

#include "XmlInterfaces.h"
#include "XmlReader.h"

namespace Xml
{

    class ElementFactory
    {
    public:
        static HElement createElement(const std::string &name)
        {
            HElement hElem{dynamic_cast<IElement *>(new TinyElement(name))};
            return hElem;
        }
    };
}

And then we can create a TinyElement object like this:

Xml::HElement root(Xml::ElementFactory::createElement("Scene"));
root->setAttribute("width", to_string(scene.getWidth()));
root->setAttribute("height", to_string(scene.getHeight()));

The concrete type TinyElement is completely hidden from the user. This is a good example of how interfaces can be used hand-in-hand with factories to hide implementation details.


7. XML Reader and Writer

7.1 Introduction

These are just simple classes that read and write XML. We will use the Element classes we just implemented to read and write XML.

7.2 Design Considerations

  1. Static Methods: Both XmlReader and XmlWriter use static methods, which means that you don't need to create an instance of these classes to use their functionalities. This decision was made for simplicity since creating an instance might be redundant for one-off operations like reading and writing XML. This makes it easier for users of the library, as they can just call the methods directly.

  2. Stream-Based Input/Output: Both classes accept standard C++ stream objects (std::istream and std::ostream) for reading and writing. This allows the library to work with any kind of stream, be it a file, a string stream, or even network streams. This design is highly flexible.

  3. Dependency on Element: The reader and writer heavily rely on the Element class to manipulate XML content. The reader reads from an input stream and produces an element (or hierarchy of elements), and the writer does the reverse. This tight coupling ensures consistency across the library and reduces the likelihood of mismatches between reading and writing.

  4. Separation of Concerns: XmlWriter has separated the task of writing individual elements and their attributes into different private methods (writeElement and writeAttributes). This makes the code modular and easier to maintain.

7.3 Implementation of XmlReader

Take a look at XmlReader.h:

#pragma once

#include "Element.h"
#include <iostream>

namespace Xml
{

    class XmlReader
    {
    public:
        static HElement loadXml(std::istream &in);
    };

}

And then XmlReader.cpp:

#include "XmlReader.h"

#include <memory>
#include <sstream>

namespace Xml
{

    HElement XmlReader::loadXml(std::istream& in)
    {
        const std::istreambuf_iterator<char> eos;
        std::string xmlStr(std::istreambuf_iterator<char>(in), eos);

        HElement hElem = ElementFactory::createElement("Scene"); // Name here is just a placeholder; it'll be overwritten when loading from XML.
        hElem->createFromXml(xmlStr);

        return hElem;
    }

}

Several things to note:

  1. Loading XML: The loadXml method reads from the provided input stream until it hits the end, storing the entire XML content into a string (xmlStr).

  2. Creating Element: A placeholder HElement with the name "Scene" is created. This name is just a placeholder, and it will be overwritten when the XML content is loaded into it.

  3. Parsing XML: The createFromXml method from the Element class is used to parse the XML string and fill in the details in the HElement. This parsed HElement (which can be a hierarchy of multiple elements) is then returned.

7.4 Implementation of XmlWriter

Take a look at XmlWriter.h:

#pragma once

#include "Element.h"
#include <ostream>

namespace Xml
{

    class XmlWriter
    {
    public:
        static void writeXml(const HElement &element, std::ostream &os);

    private:
        static void writeElement(const HElement &element, std::ostream &os);
        static void writeAttributes(const HElement &element, std::ostream &os);
    };

}

And then XmlWriter.cpp:

#include "XmlWriter.h"
#include <algorithm>

namespace Xml
{

    void XmlWriter::writeXml(const HElement &element, std::ostream &os)
    {
        return writeElement(element, os);
    }

    void XmlWriter::writeElement(const HElement &element, std::ostream &os)
    {
        os << "<" << element->getName();
        writeAttributes(element, os);

        ElementCollection childElements{element->getChildElements()};

        if (!childElements.empty())
        {
            os << ">" << std::endl;
        }
        else
        {
            os << "/>" << std::endl;
            return;
        }

        std::for_each(std::begin(childElements), std::end(childElements), [&os](const auto &elem)
                      { writeElement(elem, os); });

        os << "</" << element->getName() << ">" << std::endl;
    }

    void XmlWriter::writeAttributes(const HElement &element, std::ostream &os)
    {
        AttributeMap attributes = element->getAttributes();

        std::for_each(std::begin(attributes), std::end(attributes), [&os](const auto &attr)
                      { os << " " << attr.first << "=\"" << attr.second << "\""; });
    }

}

Serveral things to note:

  1. Writing XML: The writeXml method is the starting point which delegates the writing to the writeElement method.

  2. Serializing Individual Element: The writeElement method handles the task of serializing individual XML elements. It starts by writing the opening tag and then the attributes (by calling writeAttributes). Depending on whether the element has child elements or not, it will either close the tag immediately (self-closing tag) or write the child elements and then close the tag.

  3. Serializing Attributes: The writeAttributes method loops through each attribute of the given element and writes it in the format key="value".

  4. Recursive Writing: Notice that the method writeElement is recursive. If an element has child elements, it calls itself for each child. This ensures that the entire XML hierarchy is correctly serialized, no matter how deep or complex.


8. Scene Reader and Writer

8.1 Introduction

We just implemented the Element classes. Let's see how we can apply them to serialize and deserialize a Scene object.

8.2 Design Considerations

  1. Single Responsibility Principle (SRP): Each function in SceneReader and SceneWriter focuses on parsing or creating a specific component of the Scene structure. This approach keeps each function concise and adheres to the Single Responsibility Principle.

  2. Exception Handling: Rigorous error-checking ensures that any malformatted or unexpected content in the XML leads to a meaningful error. For instance, checking if a VectorGraphic is open or closed and ensuring PlacedGraphic is within scene boundaries.

  3. Encapsulation: Using an anonymous namespace for helper functions ensures that they remain private to the translation unit (SceneReader.cpp). This avoids potential naming conflicts and encapsulates utility functions, keeping the global namespace clean.

  4. Consistent XML Element Naming: The code expects specific XML element names like "Scene", "Layer", "PlacedGraphic", etc. This ensures a standardized XML structure and makes the parsing deterministic.

  5. Recursive Approach: This recursive design makes it easier to handle the inherent nested structure of a Scene XML, which can consist of multiple layers, and each layer can have multiple graphics.

8.3 Implementation of SceneReader

Take a look at SceneReader.h:

#pragma once

#include "XmlInterfaces.h"
#include "Scene.h"

namespace VG
{
    class SceneReader
    {
    public:
        static VG::Scene readScene(const Xml::IElement& rootElement);
    };
}

And then SceneReader.cpp:

#include "SceneReader.h"
#include "Scene.h"
#include "XmlInterfaces.h"
#include "VectorGraphic.h"

#include <sstream>

namespace
{
    int toInt(const std::string &s)
    {
        int value;
        std::stringstream ss(s);
        ss >> value;

        return value;
    }

    VG::VectorGraphic readVectorGraphic(const Xml::HElement &vgElement)
    {
        VG::VectorGraphic vg;

        const std::string closed = vgElement->getAttribute("closed");
        if (closed == "true")
        {
            vg.closeShape();
        }
        else if (closed == "false")
        {
            vg.openShape();
        }
        else
        {
            throw std::runtime_error("Invalid VectorGraphic attribute");
        }

        const auto points = vgElement->getChildElements();

        for (auto &&p : points)
        {
            const auto x = toInt(p->getAttribute("x"));
            const auto y = toInt(p->getAttribute("y"));
            vg.addPoint(VG::Point(x, y));
        }

        return vg;
    }

    void readGraphic(VG::Scene &scene,
                     VG::Layer &layer,
                     const Xml::HElement &graphicElement)
    {
        if (graphicElement->getName() != "PlacedGraphic")
        {
            throw std::runtime_error("Expected PlacedGraphic");
        }

        VG::PlacedGraphic pg;
        const int x = toInt(graphicElement->getAttribute("x"));
        const int y = toInt(graphicElement->getAttribute("y"));

        if (x < 0 || y < 0 ||
            x > scene.getWidth() || y > scene.getHeight())
        {
            throw std::runtime_error("PlacedGraphic out of bounds");
        }

        pg.setPlacementPoint(VG::Point(x, y));

        const auto vectorGraphics = graphicElement->getChildElements();

        if (vectorGraphics.size() > 1u)
        {
            throw std::runtime_error("PlacedGraphic: too many VectorGraphic nodes");
        }

        for (auto &&vgElement : vectorGraphics)
        {
            pg.setGraphic(std::make_shared<VG::VectorGraphic>(readVectorGraphic(vgElement)));
        }

        layer.insertGraphic(pg);
    }

    void readLayer(VG::Scene &scene,
                   const Xml::HElement &layerElement)
    {
        if (layerElement->getName() != "Layer")
        {
            throw std::runtime_error("Expected Layer");
        }

        VG::Layer layer(layerElement->getAttribute("alias"));

        const auto graphics = layerElement->getChildElements();

        for (auto &&graphic : graphics)
        {
            readGraphic(scene, layer, graphic);
        }

        scene.insertLayer(layer);
    }
}

namespace VG
{

    Scene SceneReader::readScene(const Xml::IElement &rootElement)
    {
        if (rootElement.getName() != "Scene")
        {
            throw std::runtime_error("Expected Scene");
        }

        const int width = toInt(rootElement.getAttribute("width"));
        const int height = toInt(rootElement.getAttribute("height"));
        Scene theScene(width, height);

        const auto layers = rootElement.getChildElements();

        for (auto &&layer : layers)
        {
            readLayer(theScene, layer);
        }

        return theScene;
    }

}

Notice a few things:

  • We use an anonymous namespace to hide the helper functions from the rest of the code. This is a good practice to avoid polluting the global namespace.
  • The toInt function is a helper function that converts a string to an integer. It's used to convert the width and height attributes from the XML to integers.
  • SceneReader::readScene is the main function that works by recursively calling the helper functions: readLayer, readGraphic, and readVectorGraphic to parse the XML:
    • The readVectorGraphic reads and interprets a VectorGraphic from an XML element. It handles the attributes of the VectorGraphic, like if it's open or closed, and then reads the points that make up the graphic.
    • The readGraphic interprets a PlacedGraphic from an XML element. It makes sure the graphic is placed within the bounds of the scene. If the placement is valid, it reads the associated VectorGraphic.
    • The readLayer interprets a Layer from an XML element. Each layer can have multiple graphics, so it reads and processes each graphic in the layer.

8.4 Implementation of SceneWriter

Take a look at SceneWriter.h:

#pragma once

#include "XmlInterfaces.h"
#include "Scene.h"

namespace VG
{
    class SceneWriter
    {
    public:
        static Xml::HElement writeScene(const Scene &scene);
    };
}

And then SceneWriter.cpp:

#include "XmlInterfaces.h"
#include "XmlFactories.h"
#include "SceneWriter.h"
#include "VectorGraphic.h"

#include <sstream>
#include <string>

using namespace std;

namespace VG
{

    Xml::HElement SceneWriter::writeScene(const Scene &scene)
    {
        Xml::HElement root(Xml::ElementFactory::createElement("Scene"));
        root->setAttribute("width", to_string(scene.getWidth()));
        root->setAttribute("height", to_string(scene.getHeight()));

        for (auto &&layer : scene)
        {
            Xml::HElement layerElement(root->appendChild("Layer"));
            layerElement->setAttribute("alias", layer.getAlias());

            Layer::PlacedGraphicIterator iPlacedGraphic;
            for (iPlacedGraphic = layer.begin(); iPlacedGraphic != layer.end(); ++iPlacedGraphic)
            {
                const PlacedGraphic &placedGraphic = *iPlacedGraphic;
                Xml::HElement placedGraphicElement(layerElement->appendChild("PlacedGraphic"));
                const auto &placementPoint = placedGraphic.getPlacementPoint();
                placedGraphicElement->setAttribute("x", to_string(placementPoint.getX()));
                placedGraphicElement->setAttribute("y", to_string(placementPoint.getY()));

                Xml::HElement vectorGraphicElement(placedGraphicElement->appendChild("VectorGraphic"));
                const auto &vectorGraphic = placedGraphic.getGraphic();

                ostringstream ss;
                ss << boolalpha << vectorGraphic->isClosed();
                vectorGraphicElement->setAttribute("closed", ss.str());

                for (auto i = 0u; i < vectorGraphic->getPointCount(); ++i)
                {
                    const auto &p = vectorGraphic->getPoint(i);
                    Xml::HElement pointElement(vectorGraphicElement->appendChild("Point"));
                    pointElement->setAttribute("x", to_string(p.getX()));
                    pointElement->setAttribute("y", to_string(p.getY()));
                }
            }
        }

        return root;
    }

}

This is the reverse of the SceneReader. Its role is to take a Scene object and produce an XML representation.

  1. Data Conversion: to_string is frequently used to convert integer data back into string format suitable for XML attributes.

  2. Vector Graphics Writing: Each VectorGraphic is translated into an XML format, with attributes like whether it's open or closed and its constituent points.

  3. Placed Graphics Writing: For each PlacedGraphic, its position and associated VectorGraphic are written to XML.

  4. Layer Writing: The entire collection of PlacedGraphics within a layer gets written to its XML representation.

  5. Scene Writing: Lastly, writeScene coordinates the entire process. Starting with scene dimensions, it systematically traverses each layer and writes it to the XML format.