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 ofLayer
s. Also haswidth
andheight
properties.Layer
: A collection ofPlacedVectorGraphic
s. Also has a name.PlacedVectorGraphic
: AVectorGraphic
with aPoint
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 andElementFactory
to hide the implementation details in case we want to switch to another XML library in the future.
XmlReader
: reads an XML file into anIElement
DOM object.XMLWriter
: writes anIElement
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 anIElement
DOM object (that is already built byXmlReader
) to create aScene
object.SceneWriter
: Serialize aScene
object to anIElement
DOM 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 Layer
s. 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
, andstd::deque
. Usingstd::list
for managing Layers within a Scene object makes a lot of sense for several reasons. The linked-list nature ofstd::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 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 PlacedVectorGraphic
s. 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 whichPlacedVectorGraphic
is on top. This is important for drawing and hit testing. We can do this by using astd::vector
to store thePlacedVectorGraphic
s, and then usestd::sort
to sort them by theirdepth
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 multiplePlacedVectorGraphic
objects. To make it clear, we use theH
prefix to indicate that this is a handle to aVectorGraphic
object. friend
method for comparing equality: We need to be able to comparePlacedVectorGraphic
objects 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 afriend
method to compare the private members of twoPlacedVectorGraphic
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:
-
Inside the class:
friend bool operator==(const PlacedGraphic &lhs, const PlacedGraphic &rhs);
This line declares theoperator==
function as afriend
of thePlacedGraphic
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. Thefriend
keyword 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 ofPlacedGraphic
due to thefriend
declaration 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
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 theIElement
interface. It has methods for getting and setting attributes, getting child elements, etc. We will implement this interface using TinyXML2 in theTinyElement
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 atinyxml2::XMLElement
. This might be used internally by the class to create aTinyElement
directly from an existingtinyxml2
element, without exposing this capability to external users. It’s a way to encapsulate the usage oftinyxml2
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 associatedTinyElement
objects exist. It usesshared_ptr
to manage the ownership and lifetime of theXMLDocument
. -
tinyxml2::XMLElement *myElement;
: This is a direct pointer to the specific XML element that thisTinyElement
instance wraps. It provides direct access to the underlyingtinyxml2
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
-
Static Methods: Both
XmlReader
andXmlWriter
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. -
Stream-Based Input/Output: Both classes accept standard C++ stream objects (
std::istream
andstd::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
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. -
Separation of Concerns:
XmlWriter
has separated the task of writing individual elements and their attributes into different private methods (writeElement
andwriteAttributes
). 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
loadXml
method reads from the provided input stream until it hits the end, storing the entire XML content into a string (xmlStr
). -
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. -
Parsing XML: The
createFromXml
method from theElement
class 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
writeXml
method is the starting point which delegates the writing to thewriteElement
method. -
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 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
writeAttributes
method loops through each attribute of the given element and writes it in the formatkey="value"
. -
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
-
Single Responsibility Principle (SRP): Each function in
SceneReader
andSceneWriter
focuses on parsing or creating a specific component of theScene
structure. 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
VectorGraphic
is open or closed and ensuringPlacedGraphic
is 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
toInt
function is a helper function that converts a string to an integer. It's used to convert thewidth
andheight
attributes from the XML to integers. SceneReader::readScene
is the main function that works by recursively calling the helper functions:readLayer
,readGraphic
, andreadVectorGraphic
to parse the XML:- The
readVectorGraphic
reads and interprets aVectorGraphic
from 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
readGraphic
interprets aPlacedGraphic
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 associatedVectorGraphic
. - The
readLayer
interprets aLayer
from 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_string
is 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 associatedVectorGraphic
are written to XML. -
Layer Writing: The entire collection of
PlacedGraphics
within a layer gets written to its XML representation. -
Scene Writing: Lastly,
writeScene
coordinates the entire process. Starting with scene dimensions, it systematically traverses each layer and writes it to the XML format.