В конце 90-х годов работал в одной организации, которая занималась развитием контактной электрической сети и эксплуатацией питающих энергетических установок (тяговых подстанций), плюс осуществляла мониторинг и управление этого хозяйства с помощью специализированного программно-аппаратного комплекса из нескольких диспетчерских пунктов. Комплекс работал под управлением АСУ ТП на древнем советском железе. Тогда стояла задача перевести этот комплекс под windows, включая разработку графического интерфейса, который бы отображал схематически в виде карты всю сеть и события происходящие на ней и подстанциях. Ну и естественно, предоставлял бы возможность управлять ими.
Технически задача заключалась в том, чтобы по сети (IP) получить данные (пакет-сообщение), распарсить его (получить данные о подстанции и изменениях на ней) и в итоге отобразить все это в нашем UI клиенте (отобразить изменения на карте/схеме, например поменять цвет элемента и заставить его мигать). Каждая подстанция обладала определенным набором датчиков и счетчиков, данные которых и содержались в пакетах-сообщениях. Необходимо было т. ж. реализовать возможность выбора на карте подстанции и отобразить ее в схематическом виде с интерактивными управляющими элементами, с помощью которых оператор мог бы посылать команды.
Как вы понимаете графические элементы были довольно специфичные и соответственно использовать стандартные не представлялось возможным, кроме того по сути надо было реализовать анимацию. И тогда было выбрано решение с использованием double buffering, когда при изменениях прорисовывался соответствующий кадр и происходила их смена с определенной частотой. Первая реализация была выполнена на Delphi. Но в процессе эксплуатации мы столкнулись с тем, что часто было необходимо менять отображение конфигурации оборудования и сети. Что в свою очередь каждый раз требовало вмешательство в исходный код (писать тогда какой либо конфигуратор на Delphi было лениво) да и как то не очень красиво выходило). И в 1999 году я первый раз познакомился с Java. Меня зацепила тогда технология reflection с помощью которой был создан простой скриптовый язык (похожий на Groovy, я тогда про него правда и не слышал, да и не мог, т.к. Groovy появился в 2003)) для описания схемы. Он так же как и Groovy использует объекты Java.
Ниже приведен фрагмент скрипта где Tp, Bus, Lab, Led, Box не что иное как Java-классы обёртки над графическими примитивами позволяющие реализовать описанный выше в задаче функционал.
Root.txt
Здесь, например, метод recall, который ссылается на первый аргумент define — аналог evaluate в Groovy). Посмотрим, что там происходит:
src/1.txt
src/2.txt
В этих файлах описаны два самых верхних устройства (Рис.1) и как можно заметить, присутствует некая повторяемость кода. Соответственно таким образом определяются шаблоны для всех комплексных блоков/устройств и затем пере-используются.
Рис. 1. Результат работы фрагмента скрипта
Рис. 2. Полная карта контактной сети и подстанций
Приложение в таком виде эксплуатируется до сих пор (хотя за это время сменилось несколько поколений бэкенда!) и заказчика полностью устраивает, но меня все время подсознательно не покидала мысль избавиться от существующего “велосипеда”, хотя хочу отметить что в 99-м году он таковым еще не являлся. И вот, с появлением Spring 4 и очередного контекста на Groovy, покурив мануал мне показалось, что я где-то уже это видел)) (см. выше). Было решено, Spring Boot, Groovy-конфиг и JavaFx — новый стек для нового GUI — клиента…
Давайте рассмотрим архитектуру нового решения. И начнем с модели, которая представляет собой абстрактную обертку вокруг графических примитивов javafx и по сути является ядром приложения. Включает в себя ряд классов и интерфейсов. Абстрактный класс Unit является базовым для создания кастомного графического элемента из которых формируется карта и схемы.
Он имплементирует интерфейс Render, который управляет изменениями изображения
, где render() — отрисовка после изменения состояния;
next() — сменить кадр.
Каждый Unit имеет список List lays вложенных Unit, что позволяет отрисовывать и управлять сценой по слоям. Он так же содержит методы для перехвата некоторых событий мыши.
В нашем случае Unit имеет наследника FlashingUnit:
, который как раз реализует смену кадров (мигание измененных объектов) в соответствии с нашим заданием.
В качестве примера привожу реализацию текстового графического элемента:
За состояние графических элементов отвечает класс State:
, который хранит информацию о состоянии и ее изменении всех элементов с общим code находящихся в List slaves и обеспечивает потокобезопасное обновление подчиненных элементов посредством интерфейса Render.
Класс Controller расширяет Unit и является контейнером для всех наших графических элементов и состояний объектов принадлежащих в данном случае отдельной подстанции с уникальным
Ну и наконец класс Root, который содержит маппинг всех контроллеров (подстанций):
Здесь хочется отметить, что использование @PostConctruct init() и DI в Controller позволяет нам динамически создавать конфигурацию состояний объектов и избавить пользователя от лишнего кода в скрипте конфигурации.
А теперь давайте посмотрим как это все выглядит в Groovy — конфиге, собственно для чего все это мы и создавали:
root.groovy
srcs.groovy
src.groovy
Теперь, используя возможности Groovy, мы можем создавать различные конфигурации схем оборудования из имеющихся графических примитивов без переборки проекта. И эффективно переиспользовать имеющийся код(я специально показал здесь файлы srcs.groovy и src.groovy). Исходники прототипа лежат здесь.
Современный стек технологий Java позволяет реализовать эффективные и нестандартные вещи без использования “велосипедов”, относительно легко и в разумные сроки. Не зная Groovy да и собственно JavaFx, прототип нового приложения был реализован на “коленках” за несколько дней. И, что еще немаловажно — мы создали наше приложение с использованием мощных и открытых стандартов производственной разработки Java.
Описание задачи
Технически задача заключалась в том, чтобы по сети (IP) получить данные (пакет-сообщение), распарсить его (получить данные о подстанции и изменениях на ней) и в итоге отобразить все это в нашем UI клиенте (отобразить изменения на карте/схеме, например поменять цвет элемента и заставить его мигать). Каждая подстанция обладала определенным набором датчиков и счетчиков, данные которых и содержались в пакетах-сообщениях. Необходимо было т. ж. реализовать возможность выбора на карте подстанции и отобразить ее в схематическом виде с интерактивными управляющими элементами, с помощью которых оператор мог бы посылать команды.
Реализация
Как вы понимаете графические элементы были довольно специфичные и соответственно использовать стандартные не представлялось возможным, кроме того по сути надо было реализовать анимацию. И тогда было выбрано решение с использованием double buffering, когда при изменениях прорисовывался соответствующий кадр и происходила их смена с определенной частотой. Первая реализация была выполнена на Delphi. Но в процессе эксплуатации мы столкнулись с тем, что часто было необходимо менять отображение конфигурации оборудования и сети. Что в свою очередь каждый раз требовало вмешательство в исходный код (писать тогда какой либо конфигуратор на Delphi было лениво) да и как то не очень красиво выходило). И в 1999 году я первый раз познакомился с Java. Меня зацепила тогда технология reflection с помощью которой был создан простой скриптовый язык (похожий на Groovy, я тогда про него правда и не слышал, да и не мог, т.к. Groovy появился в 2003)) для описания схемы. Он так же как и Groovy использует объекты Java.
Ниже приведен фрагмент скрипта где Tp, Bus, Lab, Led, Box не что иное как Java-классы обёртки над графическими примитивами позволяющие реализовать описанный выше в задаче функционал.
Root.txt
setFormFactor:4 4
setResolution:700 800
setScale:0.45
setScaleStep:0.1
define:"ТУ" "contr.txt"
define:"Ввод1" "src/1.txt"
define:"Ввод2" "src/2.txt"
define:"Ввод3" "src/3.txt"
define:"АГР1" "dev/1.txt"
define:"АГР2" "dev/2.txt"
define:"АГР3" "dev/3.txt"
define:"АГР4" "dev/4.txt"
define:"ЗВ" "sw/0.txt"
define:"ЛВ1" "sw/1.txt"
define:"ЛВ2" "sw/2.txt"
define:"ЛВ3" "sw/3.txt"
define:"ЛВ4" "sw/4.txt"
define:"ЛВ5" "sw/5.txt"
define:"ЛВ6" "sw/6.txt"
add:new Tp 31 {
busWidth:7
setColor:col.red col.green
add:new Bus 4 0 0 2 523 106 627 160
add:new Bus 5 0 0 2 636 164 683 190
add:new Bus 6 0 0 2 689 195 741 243
add:new Bus 29 0 0 2 730 268 650 268
setColor:col.black col
add:new Led "" 627 157 8
add:new Led "" 682 187 8
add:new Led "" 739 239 8
add:new Led "" 734 268 8
setFontSize:21
add:new Lab "5311" 590 113
add:new Lab "5312" 670 159
add:new Lab "5313" 714 187
add:new Lab "5314" 660 280
ico:655 123
box {
setFontSize:24
add:new Lab "(1802)" 70 5
recall:"ТУ"
setFontSize:17
setColor:col.red col.black
add:new Lab 18 400 200
recall:"Ввод1"
setColor:col.white col
add:new Box "" 10 50 10 10 {
setColor:col.black col
setFontSize:12
ins:new Lab "яч.5 ПП-1808 116" 5 0
}
recall:"Ввод2"
setColor:col.white col
add:new Box "" 130 50 10 10 {
setColor:col.black col
ins:new Lab "яч.6 ПП-1808 206" 5 0
}
busWidth:10
setColor:col.blue col
add:new Bus "" 0 0 2 30 180 270 180
recall:"АГР1"
recall:"АГР2"
recall:"ЗВ"
recall:"ЛВ1"
recall:"ЛВ2"
recall:"ЛВ3"
recall:"ЛВ4"
moveX:74
busWidth:10
setColor:col.blue col
add:new Bus "" 0 0 2 40 300 PX 300
}
}
Здесь, например, метод recall, который ссылается на первый аргумент define — аналог evaluate в Groovy). Посмотрим, что там происходит:
src/1.txt
setColor:col.blue col
busWidth:5
add:new Bus "" 65 60 2 0 0 0 120 {
setColor:col.pink col
add:new Box 88 -45 40 95 40 {
setColor:col.red col.green
add:new Led 90 5 5 19
add:new Led 87 33 5 19
add:new Box 86 66 8 23 23 {
add:new Popup {
setFontSize:17
add:new Btn 86 23 0 {
add:new Act ON 131
setColor:col.black col
ins:new Lab "Включить" 5 5
}
add:new Btn 86 23 34 {
add:new Act OFF 131
setColor:col col.red
add:new Box 131 8 8 15 15 { add:new Act TOG "" }
setColor:col.black col
ins:new Lab "Отключить" 25 5
}
}
}
setFontSize:12
add:new Lab "МТЗ" 5 24
add:new Lab "АВР" 30 24
}
}
src/2.txt
setColor:col.blue col
busWidth:5
add:new Bus "" 180 60 2 0 0 0 120 {
setColor:col.pink col
add:new Box 94 -45 40 95 40 {
setColor:col.red col.green
add:new Led 96 5 5 19
add:new Box 92 66 8 23 23 {
add:new Popup {
setFontSize:17
add:new Btn 92 23 0 {
add:new Act ON 132
setColor:col.black col
ins:new Lab "Включить" 5 5
}
add:new Btn 92 23 34 {
add:new Act OFF 132
setColor:col col.red
add:new Box 132 8 8 15 15 { add:new Act TOG "" }
setColor:col.black col
ins:new Lab "Отключить" 25 5
}
}
}
setFontSize:12
add:new Lab "МТЗ" 5 24
}
}
В этих файлах описаны два самых верхних устройства (Рис.1) и как можно заметить, присутствует некая повторяемость кода. Соответственно таким образом определяются шаблоны для всех комплексных блоков/устройств и затем пере-используются.
Рис. 1. Результат работы фрагмента скрипта
Рис. 2. Полная карта контактной сети и подстанций
Приложение в таком виде эксплуатируется до сих пор (хотя за это время сменилось несколько поколений бэкенда!) и заказчика полностью устраивает, но меня все время подсознательно не покидала мысль избавиться от существующего “велосипеда”, хотя хочу отметить что в 99-м году он таковым еще не являлся. И вот, с появлением Spring 4 и очередного контекста на Groovy, покурив мануал мне показалось, что я где-то уже это видел)) (см. выше). Было решено, Spring Boot, Groovy-конфиг и JavaFx — новый стек для нового GUI — клиента…
Давайте рассмотрим архитектуру нового решения. И начнем с модели, которая представляет собой абстрактную обертку вокруг графических примитивов javafx и по сути является ядром приложения. Включает в себя ряд классов и интерфейсов. Абстрактный класс Unit является базовым для создания кастомного графического элемента из которых формируется карта и схемы.
public abstract class Unit<T> implements Render {
private static Logger log = LoggerFactory.getLogger(Unit.class);
private Integer code;
private State<T> state;
Node node;
private List<Unit> lays = new LinkedList<>();
public void init(Map<Integer, State> states) {
lays.forEach(e -> e.init(states));
initState(states);
}
private void initState(Map<Integer, State> states) {
if (code != null) {
State<T> state = states.get(code);
if (state == null) {
state = new State<>(code);
states.put(code, state);
}
state.addSlave(this);
this.state = state;
}
}
public void setCode(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
public State<T> getState() {
return state;
}
public void setGeom(double[] geom) {
node = createShape(geom);
initNodeEvents();
}
private void initNodeEvents() {
if (node != null) {
node.setOnMousePressed(this::mousePressed);
node.setOnMouseReleased(this::mouseReleased);
}
}
public void setLays(List<Unit> lays) {
this.lays = lays;
}
public void render(Group group) {
if (node != null) {
group.getChildren().add(node);
}
render();
lays.forEach(e -> e.render(group));
}
protected void mousePressed(MouseEvent e) {
log.debug(e.toString());
}
protected void mouseReleased(MouseEvent e) {
log.debug(e.toString());
}
protected abstract Node createShape(double[] geom);
}
Он имплементирует интерфейс Render, который управляет изменениями изображения
public interface Render {
void render();
void next();
}
, где render() — отрисовка после изменения состояния;
next() — сменить кадр.
Каждый Unit имеет список List lays вложенных Unit, что позволяет отрисовывать и управлять сценой по слоям. Он так же содержит методы для перехвата некоторых событий мыши.
В нашем случае Unit имеет наследника FlashingUnit:
public abstract class FlashingUnit<T> extends Unit<T> {
private static Logger log = LoggerFactory.getLogger(FlashingUnit.class);
@Override
public void next() {
log.debug("{} next()", this);
if (getState() != null && node != null) {
node.setVisible(!getState().isChanged() || !node.isVisible());
}
}
}
, который как раз реализует смену кадров (мигание измененных объектов) в соответствии с нашим заданием.
В качестве примера привожу реализацию текстового графического элемента:
public class Lab extends FlashingUnit<Integer> {
private Color[] color;
private String text;
private Double size;
private Text shape;
@Override
protected Node createShape(double[] geom) {
//Can't set text without graphic context
shape = new Text(geom[0], geom[1], "");
if (size != null) {
shape.setFont(Font.font(size));
}
return shape;
}
public void setText(String text) {
this.text = text;
}
public void setSize(Double size) {
this.size = size;
}
public void setColor(Color[] color) {
this.color = color;
}
@Override
public void render(Group group) {
super.render(group);
shape.setText(text);
}
@Override
public void render() {
final Color[] c = new Color[]{color[0]};
if (getState() != null) {
getState().initValue(0);
c[0] = color[getState().getValue()];
}
shape.setFill(c[0]);
}
}
За состояние графических элементов отвечает класс State:
public class State<T> implements Render {
private final int id;
private T value;
private boolean changed;
private List<Render> slaves = new LinkedList<>();
public synchronized void initValue(T value) {
if (this.value == null) {
this.value = value;
}
}
public State(int id) {
this.id = id;
}
public int getId() {
return id;
}
public synchronized T getValue() {
return value;
}
public synchronized void setValue(T value) {
if (!value.equals(this.value)) {
this.value = value;
changed = true;
render();
}
}
public synchronized boolean isChanged() {
return changed;
}
public synchronized void setChanged(boolean changed) {
this.changed = changed;
}
public void addSlave(Render unit) {
slaves.add(unit);
}
@Override
public void render() {
Platform.runLater(() -> slaves.forEach(Render::render));
}
@Override
public void next() {
Platform.runLater(() -> slaves.forEach(Render::next));
}
}
, который хранит информацию о состоянии и ее изменении всех элементов с общим code находящихся в List slaves и обеспечивает потокобезопасное обновление подчиненных элементов посредством интерфейса Render.
Класс Controller расширяет Unit и является контейнером для всех наших графических элементов и состояний объектов принадлежащих в данном случае отдельной подстанции с уникальным
id
.public class Controller extends Unit {
private int id;
private final Root root;
private final Map<Integer, State> states = new HashMap<>();
private Unit scheme;
@Autowired
public Controller(Root root) {
this.root = root;
}
@PostConstruct
void init() {
super.init(states);
if (scheme != null) {
scheme.init(states);
}
root.addController(this);
}
public void setId(int id) {
this.id = id;
}
public State getState(int code) {
return states.get(code);
}
public int getId() {
return id;
}
public void setScheme(Unit scheme) {
this.scheme = scheme;
}
public Unit getScheme() {
return scheme;
}
@Override
protected Node createShape(double[] geom) {
return null;
}
@Override
public void render() {
states.values().forEach(Render::render);
}
@Override
public void next() {
states.values().forEach(Render::next);
}
}
Ну и наконец класс Root, который содержит маппинг всех контроллеров (подстанций):
public class Root implements Render {
private final Map<Integer, Controller> controllers = new HashMap<>();
void addController(Controller controller) {
if (controllers.containsKey(controller.getId())) {
throw new DuplicateKeyException(String.format("Controller id %d already exists",controller.getId()));
}
controllers.put(controller.getId(), controller);
}
public Controller getController(int id) {
return controllers.get(id);
}
public State getState(int controllerId, int code) {
Controller controller = controllers.get(controllerId);
if (controller != null) {
return controller.getState(code);
}
return null;
}
public void render(Group group) {
controllers.values().stream().map(Controller::getScheme)
.filter(Objects::nonNull).forEach(r -> r.render(group));
}
@Override
public void render() {
controllers.values().forEach(Render::render);
}
@Override
public void next() {
controllers.values().forEach(Render::next);
}
}
Здесь хочется отметить, что использование @PostConctruct init() и DI в Controller позволяет нам динамически создавать конфигурацию состояний объектов и избавить пользователя от лишнего кода в скрипте конфигурации.
А теперь давайте посмотрим как это все выглядит в Groovy — конфиге, собственно для чего все это мы и создавали:
root.groovy
package scheme
import com.ldim.granit.ui.model.Controller
import com.ldim.granit.ui.model.shape.Box
import com.ldim.granit.ui.model.shape.Bus
import com.ldim.granit.ui.model.shape.Lab
import com.ldim.granit.ui.model.shape.Led
import javafx.scene.paint.Color
suplyColor = [Color.GREEN, Color.RED]
alarmColor = [Color.RED, Color.GREEN]
beans {
importBeans('classpath:/scheme/srcs.groovy')
importBeans('classpath:/scheme/devs.groovy')
importBeans('classpath:/scheme/sws.groovy')
importBeans('classpath:/scheme/tsns.groovy')
importBeans('classpath:/scheme/ctrls.groovy')
controller2(Controller) {
id = 2
lays = [dev1]
}
tp9scheme(Bus) {
code = 5
color = suplyColor
width = 6
geom = [653, 764, 698, 687, 701, 631]
lays = [new Bus(code: 29, color: suplyColor, width: 6, geom: [701, 631, 701, 602]),
new Bus(code: 6, color: suplyColor, width: 6, geom: [701, 602, 705, 560, 840, 591]),
new Led(geom:[4.5, 701, 631, 701, 602]),
new Lab(text: '5092', geom: [705, 684]), new Lab(text: '5094', geom: [715, 612]), new Lab(text: '5093', geom: [764, 544]),
new Lab(text: '19 микрорайон', geom: [595, 630]),
new Box(code: 129, color: alarmColor, geom: [598, 593, 10, 10]),
new Box(code: 130, color: alarmColor, geom: [598, 603, 10, 10]),
new Box(color: [Color.GRAY], geom: [608, 593, 30, 20], lays: [new Lab(size: 11, text: 'ТП9', geom: [613, 607])])]
}
tp9(Controller) {
id = 3
lays = [src1, src2, src3,
new Bus (color: [Color.BLUE], width: 8, geom: [40, 176, 380, 176]),
dev1, dev2, dev3, dev4,
new Bus (color: [Color.BLUE], width: 8, geom: [40, 276, 585, 276]),
sw0, sw1, sw2, sw3, sw4, sw5, sw6,
new Bus (code: 12, color: suplyColor, width: 8, geom: [30, 350, 615, 350]),
lbl0, lbl1, lbl2, lbl3, lbl4,
tsn1, tsn2,
reqBtn, tstBtn, secBtn, okBtn]
scheme = tp9scheme
}
}
srcs.groovy
package scheme
beanName = 'src1'
offsetX = 0
reserved = true
devCode = 1
swCode = 11
led1Code = 11
led2Code = 11
evaluate(new File("./src/main/resources/scheme/templates/src.groovy"))
beanName = 'src2'
offsetX = 120
reserved = false
devCode = 1
swCode = 11
led1Code = 11
led2Code = 11
evaluate(new File("./src/main/resources/scheme/templates/src.groovy"))
beanName = 'src3'
offsetX = 240
reserved = false
devCode = 1
swCode = 11
led1Code = 11
led2Code = 11
evaluate(new File("./src/main/resources/scheme/templates/src.groovy"))
src.groovy
package scheme.templates
import com.ldim.granit.ui.model.shape.Box
import com.ldim.granit.ui.model.shape.Bus
import com.ldim.granit.ui.model.shape.Lab
import com.ldim.granit.ui.model.shape.Led
import javafx.scene.paint.Color
devX = 40
devY = 100
beans {
"${beanName}led1"(Led) {
bean -> bean.scope = 'prototype'
code = led1Code
color = [Color.GREEN, Color.RED]
geom = [9, devX + 14 + offsetX, devY + 16]
}
"${beanName}lbl1"(Lab) {
bean -> bean.scope = 'prototype'
code = devCode
text = 'МТЗ'
size = 9
color = [Color.BLACK, Color.BLACK]
geom = [devX + 5 + offsetX, devY + 35]
}
if (reserved) {
"${beanName}led2"(Led) {
bean ->
bean.scope = 'prototype'
code = led2Code
color = [Color.GREEN, Color.RED]
geom = [9, devX + 38 + offsetX, devY + 16]
}
"${beanName}lbl2"(Lab) {
bean ->
bean.scope = 'prototype'
code = devCode
text = 'АВР'
size = 9
color = [Color.BLACK, Color.BLACK]
geom = [devX + 30 + offsetX, devY + 35]
}
}
"${beanName}btn"(Box) {
bean -> bean.scope = 'prototype'
code = swCode
color = [Color.GREEN, Color.RED]
press = true
geom = [devX + 66 + offsetX, devY + 8, 23, 23]
}
"${beanName}box"(Box) {
bean -> bean.scope = 'prototype'
code = devCode
color = [Color.GRAY, Color.PINK]
geom = [devX + offsetX, devY, 95, 40]
lays = reserved ? [ref("${beanName}btn"),
ref("${beanName}led1"), ref("${beanName}lbl1"),
ref("${beanName}led2"), ref("${beanName}lbl2")]
: [ref("${beanName}btn"),
ref("${beanName}led1"), ref("${beanName}lbl1")]
}
"${beanName}"(Bus) {
bean -> bean.scope = 'prototype'
color = [Color.BLUE]
width = 4
geom = [devX + 77 + offsetX, devY - 30, devX + 77 + offsetX, devY + 70]
lays = [ref("${beanName}box")]
}
}
Теперь, используя возможности Groovy, мы можем создавать различные конфигурации схем оборудования из имеющихся графических примитивов без переборки проекта. И эффективно переиспользовать имеющийся код(я специально показал здесь файлы srcs.groovy и src.groovy). Исходники прототипа лежат здесь.
Заключение
Современный стек технологий Java позволяет реализовать эффективные и нестандартные вещи без использования “велосипедов”, относительно легко и в разумные сроки. Не зная Groovy да и собственно JavaFx, прототип нового приложения был реализован на “коленках” за несколько дней. И, что еще немаловажно — мы создали наше приложение с использованием мощных и открытых стандартов производственной разработки Java.
Поделиться с друзьями