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 ofLayers. Also haswidthandheightproperties.Layer: A collection ofPlacedVectorGraphics. Also has a name.PlacedVectorGraphic: AVectorGraphicwith aPointfor 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:
TinyElementas concrete implementation using TinyXML2.IElementinterface andElementFactoryto hide the implementation details in case we want to switch to another XML library in the future.
XmlReader: reads an XML file into anIElementDOM object.XMLWriter: writes anIElementDOM 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 anIElementDOM object (that is already built byXmlReader) to create aSceneobject.SceneWriter: Serialize aSceneobject to anIElementDOM object (that can be written later byXmlWriter).
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:

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

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
Layeris 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, andstd::deque. Usingstd::listfor managing Layers within a Scene object makes a lot of sense for several reasons. The linked-list nature ofstd::listis 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::listcomes with some pretty handy member functions likesplice,merge,sort,reverse, andunique, 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
Layerin the future. For example, allow the user to lock or hide a layer. -
Which
PlacedVectorGraphicis on top?: We need to be able to determine whichPlacedVectorGraphicis on top. This is important for drawing and hit testing. We can do this by using astd::vectorto store thePlacedVectorGraphics, and then usestd::sortto sort them by theirdepthproperty. 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
VectorGraphicobject across multiplePlacedVectorGraphicobjects. To make it clear, we use theHprefix to indicate that this is a handle to aVectorGraphicobject. friendmethod for comparing equality: We need to be able to comparePlacedVectorGraphicobjects for equality. However, we can't do this by overloading the==operator because we don't have access to the private members ofPlacedVectorGraphic. Instead, we can use afriendmethod to compare the private members of twoPlacedVectorGraphicobjects.
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:
-
Inside the class:
friend bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
This line declares theoperator==function as afriendof thePlacedGraphicclass, 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. Thefriendkeyword is not an access specifier. It's just a way to grant access privileges to a function. -
Outside the class:
bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
This is the actual declaration of theoperator==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 ofPlacedGraphicdue to thefrienddeclaration inside the class. -
operator!=:
Theoperator!=function is also declared outside the class. Typically, it's implemented in terms of theoperator==function, like:
return !(lhs == rhs);
There's no need to make this function afriend(unless it requires access to private members, which is unlikely if it's just usingoperator==).
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
IElementinterface: 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 theIElementinterface. It has methods for getting and setting attributes, getting child elements, etc. We will implement this interface using TinyXML2 in theTinyElementclass.
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 atinyxml2::XMLElement. This might be used internally by the class to create aTinyElementdirectly from an existingtinyxml2element, without exposing this capability to external users. It’s a way to encapsulate the usage oftinyxml2and keep it internal to the implementation. -
std::shared_ptr<tinyxml2::XMLDocument> myDocument;:tinyxml2::XMLDocumentrepresents an entire XML document. Storing a shared pointer to it ensures that the XML document stays alive as long as any associatedTinyElementobjects exist. It usesshared_ptrto manage the ownership and lifetime of theXMLDocument. -
tinyxml2::XMLElement *myElement;: This is a direct pointer to the specific XML element that thisTinyElementinstance wraps. It provides direct access to the underlyingtinyxml2representation 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
-
Static Methods: Both
XmlReaderandXmlWriteruse 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. -
Stream-Based Input/Output: Both classes accept standard C++ stream objects (
std::istreamandstd::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. -
Dependency on Element: The reader and writer heavily rely on the
Elementclass 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. -
Separation of Concerns:
XmlWriterhas separated the task of writing individual elements and their attributes into different private methods (writeElementandwriteAttributes). 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:
-
Loading XML: The
loadXmlmethod reads from the provided input stream until it hits the end, storing the entire XML content into a string (xmlStr). -
Creating Element: A placeholder
HElementwith the name "Scene" is created. This name is just a placeholder, and it will be overwritten when the XML content is loaded into it. -
Parsing XML: The
createFromXmlmethod from theElementclass is used to parse the XML string and fill in the details in theHElement. This parsedHElement(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:
-
Writing XML: The
writeXmlmethod is the starting point which delegates the writing to thewriteElementmethod. -
Serializing Individual Element: The
writeElementmethod handles the task of serializing individual XML elements. It starts by writing the opening tag and then the attributes (by callingwriteAttributes). 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. -
Serializing Attributes: The
writeAttributesmethod loops through each attribute of the given element and writes it in the formatkey="value". -
Recursive Writing: Notice that the method
writeElementis 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
-
Single Responsibility Principle (SRP): Each function in
SceneReaderandSceneWriterfocuses on parsing or creating a specific component of theScenestructure. This approach keeps each function concise and adheres to the Single Responsibility Principle. -
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
VectorGraphicis open or closed and ensuringPlacedGraphicis within scene boundaries. -
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. -
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.
-
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
toIntfunction is a helper function that converts a string to an integer. It's used to convert thewidthandheightattributes from the XML to integers. SceneReader::readSceneis the main function that works by recursively calling the helper functions:readLayer,readGraphic, andreadVectorGraphicto parse the XML:- The
readVectorGraphicreads and interprets aVectorGraphicfrom an XML element. It handles the attributes of theVectorGraphic, like if it's open or closed, and then reads the points that make up the graphic. - The
readGraphicinterprets aPlacedGraphicfrom an XML element. It makes sure the graphic is placed within the bounds of the scene. If the placement is valid, it reads the associatedVectorGraphic. - The
readLayerinterprets aLayerfrom an XML element. Each layer can have multiple graphics, so it reads and processes each graphic in the layer.
- The
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.
-
Data Conversion:
to_stringis frequently used to convert integer data back into string format suitable for XML attributes. -
Vector Graphics Writing: Each VectorGraphic is translated into an XML format, with attributes like whether it's open or closed and its constituent points.
-
Placed Graphics Writing: For each
PlacedGraphic, its position and associatedVectorGraphicare written to XML. -
Layer Writing: The entire collection of
PlacedGraphicswithin a layer gets written to its XML representation. -
Scene Writing: Lastly,
writeScenecoordinates the entire process. Starting with scene dimensions, it systematically traverses each layer and writes it to the XML format.