В конце 90-х годов работал в одной организации, которая занималась развитием контактной электрической сети и эксплуатацией питающих энергетических установок (тяговых подстанций), плюс осуществляла мониторинг и управление этого хозяйства с помощью специализированного программно-аппаратного комплекса из нескольких диспетчерских пунктов. Комплекс работал под управлением АСУ ТП на древнем советском железе. Тогда стояла задача перевести этот комплекс под windows, включая разработку графического интерфейса, который бы отображал схематически в виде карты всю сеть и события происходящие на ней и подстанциях. Ну и естественно, предоставлял бы возможность управлять ими.
Описание задачи.
Технически задача заключалась в том, чтобы по сети (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.
ссылка на оригинал статьи https://habrahabr.ru/post/328402/
Добавить комментарий