§5 Векторы. Основные понятия. Класс sf::Vector2<T>. Вывод текста

Класс sf::Vector2<T>

На этом уроке мы подробно поговорим о таком важном понятии линейной алгебры как вектор. Параллельно мы будем составлять функции, которые будут использоваться нами при работе с векторами, в том числе, в качестве методов классов. Напоследок, мы разделим наш проект на несколько файлов. Вы уже поняли, что функции могут сделать программу более читабельной и лаконичной, если вынести некоторые часто выполняемые задачи в отдельные блоки и вызывать их в программе по мере необходимости. Подробнее о функциях вы можете прочитать здесь: §13 Функции не возвращающие значение. (Процедуры) и здесь: §14 Функции возвращающие значение.
Для работы с векторами в SFML используется класс Vector2 (в трехмерном пространстве Vector3). Шаблонный класс Vector2 предназначен для манипулирования 2-мерными векторами. Это простой класс, который определяет математический вектор с двумя координатами (x и y). Его можно использовать для представления всего, что имеет два измерения: размер, точку, скорость и т. п. Если Vector2 представляет точку, то это можно представить как вектор, который исходит из начала координат (точки A и B на Рис. 1). Параметр шаблона T является типом координат. Это может быть любой тип, который поддерживает арифметические операции (+, -, /, *) и сравнения (==,!=). Чаще всего – это int или float.

Рис. 1

Класс sf::Vector2 имеет небольшой и простой интерфейс. К его элементам x и y можно обращаться напрямую и он не содержит математических функций.

Операции с векторами

Чтобы построить вектор (А, Рис. 1) по двум координатам x = 3 и y = 2 необходимо передать значения в конструктор в качестве аргументов:

sf::Vector2f А(3.f, 2.f);
sf::Vector2f B(1.f, 3.f);

Перегруженная версия конструктора позволяет в качестве аргумента принимать другой объект этого класса:

sf::Vector2<T>::Vector2(const Vector2<U> &vector)

При этом тип U должен конвертироваться в тип T.
Для изменения координаты необходимо обратиться к соответствующему полю:

В.x = 1.f;

Аналогично для копирования координаты:

float y_c = B.y;

Над векторами можно производить арифметические операции по правилам линейной алгебры. Однако, для нас важны не только и не сколько векторы, сколько их проекции на оси координат, что дает нам возможность управлять объектами в двумерном пространстве. На Рис. 1 изображены два вектора A и B. Проекции вектора A: Xa и Ya. Проекция вектора AB: |Xa - Xb| и |Ya - Yb| (что такое вектор AB см. ниже). Но при этом, правила для арифметических операций, как сказано ранее, уже заложены (инкапсулированы) в классах, так что можно не обращаться к компонентам векторов (например A.x или A.y), а выполнять операции непосредственно, как показано в примерах ниже.
Умножение вектора на число:

sf::Vector2f S = A * 5.f;

Получение третьего вектора при сложение двух других:

sf::Vector2f C = A + B;

В задаче, которую мы будем решать ниже, важно не сложение двух векторов, а их разность (A - B, рис. 1). Разность двух векторов A и B образует третий вектор АВ, который определяет в каком направлении находится вектор B (иначе говоря, объект B).
Очень многие классы (SFML) используют функции для работы с координатами. Эти функции получают или возвращают объект класса Vector2. Если мы получаем или устанавливаем координаты курсора (например, при нажатие на клавишу мыши), то используется Vector2i (целочисленные координаты):

sf::Vector2i pos = sf::Mouse::getPosition(window);
sf::Mouse::setPosition(sf::Vector2i(100, 200), window);

Поскольку функции cmath работают с действительными числами, то эти координаты разумно преобразовать из целочисленных в действительные (float):

sf::Vector2f posf = sf::window.mapPixelToCoords(pos);

Но, обычно, ввиду неявного приведения типов, большой необходимости в этом нет.

Напомню, что функции библиотеки cmath возвращают тип double, если аргумент целочисленный. Для работы с типом float используется постфикс f. Например, sqrtf, cosf.

Что касается классов sf::Sprite, sf::Shape (включая дочерние), то они используют тип Vector2f. Например:

sprite.setPosition(sf::Vector2f(100, 200));
sf::Vector2f pos = sprite.getPosition();

Определение длины вектора (расстояний между объектами)

Первая функция , которую мы разработаем на этом уроке – это функция определяющая длину вектора образованного двумя векторами from и to. Параметры функции (distance) – два объекта Vector2f. В теле функции получаем разность векторов (по существу, проекции на координатные оси). Далее (учитывая, что это катеты прямоугольного треугольника) вычисляем и возвращаем гипотенузу (длину вектора АВ на Рис. 1).

Далее подразумевается, что вы включили библиотеку cmath в свою программу
float distance(sf::Vector2f from, sf::Vector2f to) {
    sf::Vector2f XY = from - to;
    return hypot(XY.x, XY.y);
}

Нормализация вектора (единичный вектор)

Нормализация – это превращение исходного вектора в единичный вектор, т. е. в вектор, длина которого равна 1. Нормализация имеет большое значение. Она используется, прежде всего, для определения вектора скорости и направления, но также упрощает решение многих задач. Как известно, единица при умножении на число не меняет результата, но вектор, помимо длины, имеет ещё и направление, а это важно. Для нормализации вектора необходимо каждую компоненту координат разделить на длину вектора. Функция norm возвращает объект класса Vector2 – нормализованный исходный вектор (принимаемый как параметр).

sf::Vector2f norm(sf::Vector2f vec) {
    float d = hypot(vec.x, vec.y);
    float norm_x = vec.x / d;
    float norm_y = vec.y / d;
    return sf::Vector2f(norm_x, norm_y);
}

В задаче, решаемой на этом уроке, нормализация используется для определения вектора направления (см. программу sf.5.1 стр. 108).

Преобразования Radians <=> Degree

В программах часто приходится переводить из одной угловой меры в другую, так как функции SFML (например, setRotation()) принимают аргумент в градусной мере, а функции библиотеки cmath принимают и возвращают радианы. В системе СИ радиан (русское: рад, международное: rad) — это угол, соответствующий дуге, длина которой равна её радиусу. Ниже приводятся функции составленные на основании того факта, что a[°] = α[рад] × (180° / π), а α[рад] = a[°] × (π / 180°).

// Из рад. в градусы:
float RadToDeg(float rad) {
    return rad * 180 / M_PI;
}

// Из градусов в рад.:
float DegToRad(float deg) {
    return deg * M_PI / 180;
}

Поворот спрайта с помощью вектора направления и tan2

Для того, чтобы повернуть спрайт в направлении какого-либо объекта или координаты события (в нашем случае это окружность, появляющаяся в случайных координатах) необходимо определить угол, который будет вычисляться с помощью проекций векторов на оси координат. Как мы уже сказали – это катеты. Следовательно, тригонометрическая функция (библиотеки cmath), которую мы будем использовать – это арктангенс (atan), но в её специальной реализации atan2 с двумя аргументами. Фактически atan2 исправляет определение угла по всем координатным четвертям (в отличие от tan, который принимает только один аргумент) и, таким образом, может использоваться как компонент (получения угла Theta) при переходе от прямоугольной системы координат к полярной. При этом направление на Е (восток) это начало отсчета 0, а направление увеличения угла – по часовой стрелке (к югу). Нам удобнее было поменять точку отсчета и начинать отсчет с севера (N). Однако, ввиду особенности реализации функции tan2, во II четверти будут возвращаться отрицательные углы. Для поворота это не имеет значения, но для вывода в окне имеет. Это следует поправить.
Непосредственно перед вычислением угла нужно определить разность векторов, т. е. вектор направления. Такая функция может быть реализована следующим образом:

float rot(Vector2f from, Vector2f to, char c) {
	Vector2f vec = to - from;
	float rotat = RadToDeg(atan2(vec.y, vec.x));
	switch (c) {
		case 'n' : rotat +=  90;  break;
		case 'e' : ;              break;
		case 's' : rotat -=  90;  break;
		case 'w' : rotat -= 180;  break;
	}
	return rotat;
}

Символ c обозначает какая сторона (“горизонта”) будет являться точкой отсчета угла.

Разработка проекта для проверки функций

Идея программы в следующем. В центре окна находится компас со стрелкой. В правом верхнем углу выводится текст – угол положения синего кружка (по умолчанию он находится сверху, стрелка указывает на него в позиции 0 градусов, точно на север). После нажатия клавиши пробел (Space) синий круг перемещается в новую случайную позицию. Стрелка указывает азимут положения объекта, т.е. всегда указывает на него. В правом верхнем углу выводится новое значение азимута – значение угла в градусах. Если нажать клавишу Up (стрелка вверх), то из центра компаса в направлении синего шара отправляется снаряд (кружок зеленого цвета), который в момент удара окрашивает шар тоже в зеленый цвет, при этом снаряд возвращается в исходную позицию в центре. Клавиша Delete сбрасывает все настройки по умолчанию.
Для того, чтобы появилась возможность вывода текста в окне программы, необходимо использовать классы Font и Text. Объект класса Font определяет какой будет использован шрифт и его местоположение. Непосредственно текст в программе выводится с помощью метода setString() класса Text. Он поддерживает функцию преобразования класса stringto_string(), преобразующую число в строку.
Набор свободных шрифтов DejaVu можно получить на нашем сайте по ссылке.
Программа sf.5.1

#include <SFML/Graphics.hpp>
#include <cmath>
#include <random>
#include <chrono>
#include <string>
using namespace sf;
using namespace std;
using namespace chrono;

float Rnd(float, float);
float distance(Vector2f, Vector2f);
float RadToDeg(float);
Vector2f norm(Vector2f);
float rot(Vector2f, Vector2f, char);

int main() {
	int W = 800;
	int H = 600;
	RenderWindow window(VideoMode(W, H), "lesson-5-1");

	// Настройки вывода текста
	Font font;
	if (!font.loadFromFile("./font/DejaVuSans.ttf"))
		 return -1;
	Text text("0.0", font);
	text.setCharacterSize(30);
	text.setStyle(Text::Bold);
	text.setFillColor(Color::Black);
	text.setPosition(600, 50);

	// Текстура компаса
	Texture texture;
	if (!texture.loadFromFile("./pic/compass.png"))
		return -1;
	Sprite sprite;
	sprite.setTexture(texture);
	sprite.setPosition(W/2, H/2);
	sprite.setOrigin(250, 250);

	CircleShape Cir; // Окружность в центре
	Cir.setFillColor(Color(255, 0, 0));
	Cir.setRadius(12.);
	Cir.setOrigin(12., 12.);
	Cir.setPosition(W/2, H/2);

	CircleShape Cir2; // Окружность-снаряд
	Cir2.setFillColor(Color(0, 255, 255));
	Cir2.setRadius(13.);
	Cir2.setOrigin(13., 13.);
	Cir2.setPosition(W/2, H/2);
	Vector2f Cir2_pos;

	// Стрелка
	RectangleShape strelka(Vector2f(6, 180));
	strelka.setOrigin(3, 150);
	strelka.setPosition(W/2, H/2);
	strelka.setFillColor(Color(255, 0, 0));
	Vector2f strelka_pos = strelka.getPosition();
	float Theta {};

	CircleShape CirRnd; // Окружность в случайной позиции
	CirRnd.setFillColor(Color(0, 0, 255));
	CirRnd.setRadius(20.);
	CirRnd.setOrigin(20., 20.);
	CirRnd.setPosition(W/2, 25.);
	Vector2f CirRnd_pos;

	Clock clock;
	float speed = 1000.f;
	float dX {}, dY {};
	Vector2f vec;
	bool f = false;

	while (window.isOpen()) {
		float delta = clock.getElapsedTime().asMicroseconds();
		clock.restart();
		delta = delta / speed;
    	Event event;
        while (window.pollEvent(event)) {
            if (event.type == Event::Closed)
                window.close();

            if (event.type == Event::KeyPressed) {
            	if (event.key.code == Keyboard::Escape)
            		window.close();
            	// Если нажимаем на пробел, то синий шар 
            	// устанавливается в случайной позиции, 
            	// а стрелка автоматически указывает
            	// азимут шара, в данной реализации, на него
            	if (event.key.code == Keyboard::Space) {
            		CirRnd.setFillColor(Color(0, 0, 255));
            		float X = Rnd(20., 580.);
            		float Y = Rnd(20., 580.);
            		CirRnd.setPosition(X, Y);
            		CirRnd_pos = CirRnd.getPosition();
            		// Поворот стрелки;
            		Theta = rot(strelka_pos, CirRnd_pos, 'n');
            		strelka.setRotation(Theta);
            		// Исправляем отрицательные углы
            		if (Theta < 0) Theta = 360 + Theta;
            		// Выводим значение угла
            		text.setString(to_string(Theta));
                }
            	// Если нажата клавиша "вверх", то определяем
            	// направление выстрела
            	if (event.key.code == Keyboard::Up) {
            		Cir2_pos = Cir2.getPosition();
            		CirRnd_pos = CirRnd.getPosition();
            		vec = norm(CirRnd_pos - Cir2_pos);
            		dX = Cir2_pos.x;
            		dY = Cir2_pos.y;
            		f = true;
            	}
            	// Если нажата клавиша Delete, то
            	// сбрасываем все по умолчанию
            	if (event.key.code == Keyboard::Delete) {
            		Cir2.setPosition(W/2, H/2);
            		strelka.setRotation(0);
            		text.setString("0.0");
            		CirRnd.setFillColor(Color(0, 0, 255));
            		CirRnd.setPosition(W/2, 25.);
            		f = false;
            	}
            }
        }
        Cir2_pos = Cir2.getPosition();
        // Делаем выстрел, если клавиша Up была нажата
        if (f && distance(Cir2_pos, CirRnd_pos) > 2) {
        	dX += delta * vec.x;
        	dY += delta * vec.y;
        	Cir2.setPosition(dX, dY);
        }
        // Если наш снаряд достиг цели, то...
		if (CirRnd.getGlobalBounds().contains(Cir2_pos.x, Cir2_pos.y)) {
			// перекрашиваем синий шар в зеленый цвет
			CirRnd.setFillColor(Color::Green);
			// отменяем стрельбу
			f = false;
			// возвращаем снаряд на место
			Cir2.setPosition(W/2, H/2);
		}
		window.RenderTarget::clear(Color::White);
        window.draw(sprite);
        window.draw(Cir2);
        window.draw(Cir);
        window.draw(strelka);
        window.draw(CirRnd);
        window.draw(text);
        window.display();
	}
    return 0;
}
// Функции, которые мы использовали
// Получения случайного действительного числа
float Rnd(float z1, float z2) {
	long seed = system_clock::now().time_since_epoch().count();
	static default_random_engine rnd(seed);
	static uniform_real_distribution<float> D(z1, z2);
	return D(rnd);
}
// Определение расстояния
float distance(sf::Vector2f from, sf::Vector2f to) {
    sf::Vector2f XY = from - to;
    return hypot(XY.x, XY.y);
}
// Перевод радианы в градусы
float RadToDeg(float rad) {
    return rad * 180 / M_PI;
}
// Нормализация вектора (получение единичного вектора)
Vector2f norm(Vector2f vec) {
    float d = hypot(vec.x, vec.y);
    float norm_x = vec.x / d;
    float norm_y = vec.y / d;
    return Vector2f(norm_x, norm_y);
}
// Поворот from в сторону to. Зависит от RadToDeg
float rot(Vector2f from, Vector2f to, char c) {
	// Определяем вектор поворота
	Vector2f vec = to - from;
	float rotat = RadToDeg(atan2(vec.y, vec.x));
	switch (c) {
		// Выбираем точку отсчета
		case 'n' : rotat += 90; break;
		case 'e' : ; break;
		case 's' : rotat -= 90; break;
		case 'w' : rotat -= 180; break;
	}
	return rotat;
}

Рис. 2

Рис. 3

Текстура компаса
Новые функции:

  • setRotation() – глобальное вращение от некоторой начальной позиции. Каждое новое обращение перезаписывает предыдущий поворот (т. е. – это будет новый поворот от той же начальной позиции, в отличие от rotate(), который осуществляет поворот от текущей позиции).
  • getGlobalBounds() – запрашивает глобальный ограничивающий прямоугольник объекта (границы спрайта) в глобальных координатах. Чтобы убедиться, что объект находится в пределах границ спрайта используется в сочетании с логической функцией contains(x, y):
    sprite1.getGlobalBounds().contains(sprite2_pos.x, sprite2_pos.y)

Раздельная компиляция

Код программы получился довольно громоздкий. Использовать его для продолжения разработки стало неудобным. К тому же разработанные нами функции потребуются для решения различных задач в других проектах. Поэтому мы разобьем его на несколько файлов. Как это сделать см.: §10 Функции. Раздельная компиляция. Наш проект после реорганизации будет содержать следующие файлы (исходный текст спрятан под спойлер):

  • lesson-sfml-5-1.cpp – главный файл проекта
  • Исходный текст
    #include <SFML/Graphics.hpp>
    #include "lesson-sfml.h"
    #include <string>
    using namespace sf;
    using namespace std;
    
    int main() {
    	int W = 800;
    	int H = 600;
    	RenderWindow window(VideoMode(W, H), "lesson-5-3");
    
    	// Настройки вывода текста
    	Font font;
    	if (!font.loadFromFile("./font/DejaVuSans.ttf"))
    		 return -1;
    	Text text("0.0", font);
    	text.setCharacterSize(30);
    	text.setStyle(Text::Bold);
    	text.setFillColor(Color::Black);
    	text.setPosition(600, 50);
    
    	// Текстура компаса
    	Texture texture;
    	if (!texture.loadFromFile("./pic/compass.png"))
    		return -1;
    	Sprite sprite;
    	sprite.setTexture(texture);
    	sprite.setPosition(W/2, H/2);
    	sprite.setOrigin(250, 250);
    
    	CircleShape Cir; // Окружность в центре
    	Cir.setFillColor(Color(255, 0, 0));
    	Cir.setRadius(12.);
    	Cir.setOrigin(12., 12.);
    	Cir.setPosition(W/2, H/2);
    
    	CircleShape Cir2; // Окружность-снаряд
    	Cir2.setFillColor(Color(0, 255, 255));
    	Cir2.setRadius(13.);
    	Cir2.setOrigin(13., 13.);
    	Cir2.setPosition(W/2, H/2);
    	Vector2f Cir2_pos;
    
    	// Стрелка
    	RectangleShape strelka(Vector2f(6, 180));
    	strelka.setOrigin(3, 150);
    	strelka.setPosition(W/2, H/2);
    	strelka.setFillColor(Color(255, 0, 0));
    	Vector2f strelka_pos = strelka.getPosition();
    	float Theta {};
    
    	CircleShape CirRnd; // Окружность в случайной позиции
    	CirRnd.setFillColor(Color(0, 0, 255));
    	CirRnd.setRadius(20.);
    	CirRnd.setOrigin(20., 20.);
    	CirRnd.setPosition(W/2, 25.);
    	Vector2f CirRnd_pos;
    
    	Clock clock;
    	float speed = 1000.f;
    	float dX {}, dY {};
    	Vector2f vec;
    	bool f = false;
    
    	while (window.isOpen()) {
    		float delta = clock.getElapsedTime().asMicroseconds();
    		clock.restart();
    		delta = delta / speed;
        	Event event;
            while (window.pollEvent(event)) {
                if (event.type == Event::Closed)
                    window.close();
    
                if (event.type == Event::KeyPressed) {
                	if (event.key.code == Keyboard::Escape)
                		window.close();
                	// Если нажимаем на пробел, то синий шар 
                	// устанавливается в случайной позиции, 
                	// а стрелка автоматически указывает
                	// азимут шара, в данной реализации, на него
                	if (event.key.code == Keyboard::Space) {
                		CirRnd.setFillColor(Color(0, 0, 255));
                		float X = Rnd(20., 580.);
                		float Y = Rnd(20., 580.);
                		CirRnd.setPosition(X, Y);
                		CirRnd_pos = CirRnd.getPosition();
                		// Поворот стрелки
                		Theta = rot(strelka_pos, CirRnd_pos, 'n');
                		strelka.setRotation(Theta);
                		// Исправляем отрицательные углы
                		if (Theta < 0) Theta = 360 + Theta;
                		// Выводим значение угла
                		text.setString(to_string(Theta));
                    }
                	// Если нажата клавиша "вверх", то определяем
                	// направление выстрела
                	if (event.key.code == Keyboard::Up) {
                		Cir2_pos = Cir2.getPosition();
                		CirRnd_pos = CirRnd.getPosition();
                		vec = norm(CirRnd_pos - Cir2_pos);
                		dX = Cir2_pos.x;
                		dY = Cir2_pos.y;
                		f = true;
                	}
                	// Если нажата клавиша Delete, то
                	// сбрасываем все по умолчанию
                	if (event.key.code == Keyboard::Delete) {
                		Cir2.setPosition(W/2, H/2);
                		strelka.setRotation(0);
                		text.setString("0.0");
                		CirRnd.setFillColor(Color(0, 0, 255));
                		CirRnd.setPosition(W/2, 25.);
                		f = false;
                	}
                }
            }
            Cir2_pos = Cir2.getPosition();
            // Делаем выстрел, если клавиша Up была нажата
            if (f && distance(Cir2_pos, CirRnd_pos) > 2) {
            	dX += delta * vec.x;
            	dY += delta * vec.y;
            	Cir2.setPosition(dX, dY);
            }
            // Если наш снаряд достиг цели, то...
    		if (CirRnd.getGlobalBounds().contains(Cir2_pos.x, Cir2_pos.y)) {
    			// перекрашиваем синий шар в зеленый цвет
    			CirRnd.setFillColor(Color::Green);
    			// отменяем стрельбу
    			f = false;
    			// возвращаем снаряд на место
    			Cir2.setPosition(W/2, H/2);
    		}
    		window.RenderTarget::clear(Color::White);
            window.draw(sprite);
            window.draw(Cir2);
            window.draw(Cir);
            window.draw(strelka);
            window.draw(CirRnd);
            window.draw(text);
            window.display();
    	}
        return 0;
    }
    
  • lesson-sfml.h – заголовочный файл
  • Исходный текст
    #ifndef LESSON_SFML_H_
    #define LESSON_SFML_H_
    #include <SFML/Graphics.hpp>
    float Rnd(float, float);
    float distance(sf::Vector2f, sf::Vector2f);
    float RadToDeg(float);
    sf::Vector2f norm(sf::Vector2f);
    float rot(sf::Vector2f, sf::Vector2f, char);
    
    #endif /* LESSON_SFML_H_ */
    
  • lesson-sfml.cpp – сборник функций
  • Исходный текст
    #include "lesson-sfml.h"
    #include <SFML/Graphics.hpp>
    #include <cmath>
    #include <random>
    #include <chrono>
    
    using namespace sf;
    using namespace std;
    using namespace chrono;
    
    // Функции, которые мы использовали
    // Получения случайного действительного числа
    float Rnd(float z1, float z2) {
    	long seed = system_clock::now().time_since_epoch().count();
    	static default_random_engine rnd(seed);
    	static uniform_real_distribution<float> D(z1, z2);
    	return D(rnd);
    }
    // Определение расстояния
    float distance(Vector2f from, Vector2f to) {
        Vector2f XY = from - to;
        return hypot(XY.x, XY.y);
    }
    // Перевод радианы в градусы
    float RadToDeg(float rad) {
        return rad * 180 / M_PI;
    }
    // Нормализация вектора (получение единичного вектора)
    Vector2f norm(Vector2f vec) {
        float d = hypot(vec.x, vec.y);
        float norm_x = vec.x / d;
        float norm_y = vec.y / d;
        return Vector2f(norm_x, norm_y);
    }
    // Поворот from в сторону to. Зависит от RadToDeg
    float rot(Vector2f from, Vector2f to, char c) {
    	Vector2f vec = to - from;
    	float rotat = RadToDeg(atan2(vec.y, vec.x));
    	switch (c) {
    		case 'n' : rotat += 90; break;
    		case 'e' : ; break;
    		case 's' : rotat -= 90; break;
    		case 'w' : rotat -= 180; break;
    	}
    	return rotat;
    }
    

Задания для самостоятельной работы

  • Дана карта – клетчатое поле размером 1000×1000 px на прозрачной основе. Ширина и высота ячейки 50×50, т. е. всего 20×20 = 400 клеток. Разработайте на этом поле игру типа игры в фишки. Условие придуймайте сами. Фишка должна перемещаться по клеткам на определенное количество шагов (пермещения только по горизонтали и вертикали). Предусмотреть мины, отнимающие очки или призовые позиции, добавляющие очки. Значения набранных очков выводить в правом верхнем углу. Окрасить траектории и прочие элементы карты только средствами SFML. Версии сеток:
    Белая
    Черная
Оцените материал
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...

Print Friendly, PDF & Email

Comments are closed.