Рисование SVG
Пример SVG изображения:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<rect x="10" y="10" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"/>
</svg>
Справочник по элементам и атрибутам SVG
Диаграмма классов
Это те классы, которые должны получиться в окончательной версии программы:
Условие задачи
- Вспомогательные классы для рисования
SVG
- Класс SVG, содержит открытый
PrintStream для печати SVG:
SVG svg = new SVG("a.svg", 300, 300); svg.addTag(rect1); svg.addTag(rect2); svg.close();
В конце обязательно будет вызвано закрытие, чтобы закрыть PrintStream
- Класс Tag - это описание одного тега.
Tag rect1 = new Tag("rect"); rect1.set("x", "200"); rect1.set("y", "200"); rect1.set("width", "10"); rect1.set("height", "20"); rect1.set("style", "stroke:#ff0000; fill: #0000ff");
- Класс SVG, содержит открытый
PrintStream для печати SVG:
- Когда сделаете классы Tag и SVG, проверьте, что вы можете создавть картинки с их помощью.
- Измените код так, чтобы не требовалось вручную вызывать
close()
в классе SVG. Для этого класс SVG должен реализовать интерфейс AutoClosable, и после этого класс можно будет использовать в конструкции try-with-resource:try (SVG svg = new SVG("a.svg", 300, 300)) { ...
- Давайте реализуем еще вложенные теги. Нам это понадобиться, чтобы удобно двигать фигуры. Посмотрите на такой SVG, создайте эту
картинку и посмотрите, как она выглядит:
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300"> <rect x="10" y="10" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"/> <rect x="20" y="20" height="100" width="100" style="stroke:#ff0000; fill: #00ff00"/> <g transform="translate(150, 150)"> <rect x="10" y="10" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"/> <rect x="20" y="20" height="100" width="100" style="stroke:#ff0000; fill: #00ff00"/> </g> </svg>
В этом примере рисуются два прямоугольника друг на друге, а потом такие же два прямоугольника помещены в группу (тэг
g
), и этой группе указан атрибутtransform
, который говорит, что ее содержимое надо сместить на 150 пикселей по горизонтали и вертикали.Обратите внимание, что тэги
rect
открываются и сразу закрываются, об этом говорит слэш перед закрывающей угловой скобкой. А тэгg
только открывается, у него нет слеша перед закрывающей угловой скобкой. Зато ниже этот тэг все же закрывается тегом, который начинается со слеша:</g>
. Поэтому, предлагаю ввести три типа тэгов: открывающие, закрывающие и открывающие-закрывающие. Вот как можно описать три типа тэгов в Java с помощью нового для нас понятияenum
(перечисление):public enum TagType { OPEN, CLOSE, OPEN_AND_CLOSE; }
enum
задает тип данных, который может иметь только несколько возможных значений. Для ссылки на значения пишитеTagType.OPEN
,TagType.Close
и т.п.Предлагаю создавать подобный SVG следующим образом:
Tag rect1 = new Tag("rect"); //заполняем rect1 как первый квадрат Tag rect2 = new Tag("rect"); //заполняем rect2 как второй квадрат Tag g = new Tag("g", TagType.OPEN); g.set("transform", "translate(150, 150)"); Tag gClose = new Tag("g", TagType.Close); try (SVG svg = new SVG("a.svg", 300, 300)) { svg.addTag(rect1); svg.addTag(rect2); svg.addTag(g); svg.addTag(rect1); svg.addTag(rect2); svg.addTag(gClose); }
Вам придется добавить еще один конструктор в класс
Tag
, в котором можно указать тип тэга. - Интерфейс Shape. Фигура. Изображение будет рисоваться из большого числа фигур. Какие имеено фигуры будут использоваться для рисования, будет задаваться в настройках. Этот интерфейс имеет метод getTags(), который возвращает список тегов
List<Tag>
для рисования этой фигуры. В этот же интерфейс можно (но не обязательно, сделайте это только если вам интересно попробовать неизученную раньше возможность java) поместить метод draw(SVG), который имеет реализацию по-умолчанию, и передает все свои теги в SVG для рисования:public interface Shape { List<Tag> getTags(); default void draw(SVG svg) { //используйте getTags, чтобы получить тэги для фигуры и нарисовать их на svg. } }
Сразу реализуйте этот интерфейс классами
SmallSquare
иRedCircle
, они должны возвращать по одному тэгу, которые рисуют, соответственно, маленький квадрат и красный круг. Протестируйте создание фигур, для этого добавьте в main метод рисование фигур на SVG файл. Если вы реализовали методdraw
внутри интерфейсаShape
, то достаточно будет только вызвать его. Если не реализовали, реализуйте рисование фигуры прямо внутри методаmain
.Замечание о координатах фигур. Рисуйте фигуры так, чтобы координата (0, 0) была примерно в середине фигуры. Реальное положение фигуры будет задаваться позже. Когда вы тестировали
SmallSquare
иRedCircle
, вы, наверное, указали большие значения для коориднат фигур, чтобы они оказались ближе к центру изображения. После тестирования координаты надо исправить, чтобы центр фигуры оказался в (0, 0) или примерно в (0, 0). -
Реализуйте класс
PositionedShape
, он содержит в себе информацию о фигуре и ее координатах. Такой класс позволит нам создавать много одинаковых фигур в разных местах изображения. Возьмите из диаграммы классов содержимое классаPositionedShape
и реализуйте их. При реализации методаgetTags
необходимо вернуть закрывающий и открывающий теги<g>
c атрибутомtransform
, а между ними вставить те теги, которые возвращает методgetTags
для исходной фигуры. Другими словами,PositionedShape
оборачивает теги исходной фигуры в тегg
(в группу) и сдвигает эту группу.Не забудьте протестировать созданный класс, для этого создайте несколько фигур
SmallSquare
иRedCircle
, обернутых вPositionedShape
, и сдвинутых в разные места изображения. -
При запуске программы создайте SVG размера 400 на 400, и добавьте на нее 100 фигур
SmallSquare
и 150 фигурRedCircle
, сдвинутых в разные места изображения. Используйте классRandom
для генерации случайных чисел. Вызывайте у него метод nextInteger(int max), чтобы получить случайное целое в нужном диапазоне. -
Далее мы будем исправлять предыдущий пункт, получая настройки из файла. Для этого создайте класс
Settings
, который реализует шаблон singleton и при создании читает настройки из файлаsvg.properties
в формате properties. Пусть для примера начальная версия файла настроек будет такой:rand_seed=52345234 background=#FFCCBB width=400 height=400 draw=red_circle:150 small_square:100 # описания фигур shape.red_circle=ru.spbu.arts.semester.iv.svg.shape.RedCircle shape.small_square=ru.spbu.arts.semester.iv.svg.shape.SmallSquare
Далее будем постепенно расширять программу, добавляя в нее использование информации из этого файла.
-
Создайте в классе
Settings
методыgetWidth
иgetHeight
, которые будут возвращать значения записейwidth
иheight
из файла.Программа должна создать SVG изображение именно того размера, которое указано в настройках. Напомню, что раз
Settings
реализует шаблон singleton, то пользоваться им можно в любом месте программы следующим образом:Settings.getInstance().getWidth()
. -
Создайте в классе
Settings
методgetBackground
, который возвращает css цвет фона. Программа должна перед рисованием фигур нарисовать на изображении фон этого цвета. -
Создайте в классе
Settings
методgetRandSeed
, который возвращает число для инициализации генератора случайных чисел. Именно это число нужно передать в качестве аргумента конструктору классаRandom
. Давайте еще разрешим писать в настройкахrand_seed=auto
, в этом случае методgetRandSeed
должен возвращать 0, а генератор нужно создавать без указания seed, т.е. вызывая конструктор по-умолчаниюnew Random()
. -
Создайте в классе
Settings
методgetShapeDescription
. Он должен получать название фигуры, например,red_circle
и возвращать запись в файле настроек с описанием этой фигуры. Описание в файле настроек начинается с префиксаshape.
, например, описаниеred_circle
начинается сshape.red_circle=
. Фактически, в описании будет находится полное имя класса, реализующего эту фигуру. Итого,getShapeDescription("red_circle")
возвращает"ru.spbu.arts.semester.iv.svg.shape.RedCircle"
. -
Создайте класс
ShapeFactory
и реализуйте у него методcreate
. Он получает описание фигуры (см. пердыдущий пункт) и возвращает саму фигуру. Для начала считайте, что описание фигуры - это всегда имя класса, который ее реализует. Позже мы добавим более сложные описания. С помощью рефлексии создайте объект класса, имя которого вам передано. Если класс не существует, или существует, но не является фигурой, это приравнивается к ошибке в файле конфигурации. Пока мы не обрабатываем такие ошибки, позже нужно будет сделать вывод понятных сообщений пользователю о том, что конфигурация не верна. - Создайте в классе
Settings
методgetShapsWithCount
. Он должен возвращать, каких фигур и сколько нужно создать. Эта информация хранится в строкеdraw=
в файле конфигурации. В приведенном примере файла конфигурации должно возвращаться red_circle -> 150, small_square -> 100.