Бинго-бонго и Джимбо-джамбо, дорогие друзья!
У меня на дачке не было света 2 дня, я практически иссох и впал в спячку, но я снова здесь!
В этом посте мы начнем писать предсказания погоды и немного напишем кода, а не потыкаем мышкой! Ура! Наконец-то!
Какие прогнозы мы хотим делать
Очень простые! Пока прогнозировать будем только следующий день, а правила придумаем сами; а точнее, правил не будет. Мы просто будем выводить температуру на следующий день, абсолютно такую же, как и сегодня.
Сделаем один прикольчик, демонстрирующий возможности projectional editor.
Концепты
В данном случае мы прибегнем к крутой фиче — мы создадим концепт, который будет содержать только ссылку на исходные данные, а данные мы будем выводить как график на своем Swing компоненте. О как умеем, хотя swing я жуть как не люблю.
Создаем концепт PredictionResult, добавляем в него reference "input", которая является ссылкой на реализацию концепта в текущем scope AST! Но поскольку нам не нужны области видимости, или scope, то для нас сойдет поиск всех элементов данного типа.(Кстати, Scopes это не самая легкая тема в MPS + по ней довольно сложная документация, местами непонятная, так что я накатаю статейку про Scope тоже. Когда нибудь.) Но теперь нужно добавить идентификацию для WeatherData, изменим немного структуру и Editor аспект.
Я добавил INamedConcept после implements, и теперь у нашего концепта WeatherData есть имя, но мы его никак не присваеваем, поэтому изменим Editor.
Здесь мы просто добавили 1 строчку, которая будет содержать имя. Пересоберем язык и посмотрим, че получилось.
Ура, теперь называем эту WeatherData именем "today" и возвращаемся к концепту PredictionResult и меняем его Editor аспект.
Пока так. У нас будет отображаться Prediction for tommorow, data %name_of_weather_data%
Добавим концепт в PredictionList — наш рутовый концепт, где пока находятся только входные данные.
Если собрать, то получится
… как раз то, чего мы и хотели. Мы можем выбирать из списка WeatherData(ничего страшного, что у нас только 1 WeatherData, зато расширяемо).
Здорово, теперь нужно как-то круто выводить наши прогнозы. Я уже написал, что выводить мы их будем на swing компоненте, если кто не знает — javax.swing. — пакет для разработки нативных графических интерфейсов на Java. На нем построена IntelliJ. Swing компоненты можно юзать в editor. Уря.
Перед тем, как рисовать все это дело, распишем по пунктам, как мы будем действовать.
- Берем ширину графика в пикселях и делим ее на 60 * 24 — количество минут в дне. Это нужно для того, чтобы правильно отображать точки по оси абсцисс.
- Переводим все температуры в одну единицу измерения, например, цельсии (потом мы настроим так, чтобы можно было самим выбирать, в цельсиях показывать или в фаренгейтах) и находим наибольшую температуру и наименьшую. Вычитаем из наибольшей наименьшую и получаем полную "высоту" в градусах. Суть в том, что если мы поделим высоту графика на эту величину, то получим сколько "пикселей в одном градусе". Это потребуется для того, чтобы проецировать температуры на график.
- Сортируем массив входных данных по времени(чем ближе к 00:00 — тем меньше, естественно) и проходимся по нему. Вычисляем x по формуле
$$display$$время_в_минутах * коэффициент_из_пункта_1$$display$$
а y
$$display$$температура * коэффициент__из__пункта 2$$display$$
P.S. формулы это ужас
Рисуем!
Чтобы Вас не мучать поэтапным написанием строчек, скину весь и пройдусь по более-менее сложным местам.{ final int chartWidth = 400; final int chartHeight = 200; final JPanel panel = new JPanel() { @Override protected void paintComponent(final Graphics graphics) { super.paintComponent(graphics); editorContext.getRepository().getModelAccess().runReadAction(new Runnable() { public void run() { string unit = node.unit; final list<Point2D.Double> labels = node.input.items.where({~it => !it.temperature.concept.isAbstract(); }).select({~it => message debug "Woaw!" + it.temperature.concept.isAbstract(), <no project>, <no throwable>; double x = it.time.hours * 60 + it.time.minutes; double y = it.temperature.getValueFromUnit(unit.toString()); new Point2D.Double(x, y); }).sortBy({~it => it.x; }, asc).toList; final double minTemp = labels.sortBy({~it => it.y; }, asc).first.y; final double maxTemp = labels.sortBy({~it => it.y; }, asc).last.y; final double yKoef = chartHeight / (maxTemp - minTemp); final double xKoef = chartWidth / (60.0 * 24.0); int prevY = chartHeight; int prevX = -1; Graphics2D g2 = ((Graphics2D) graphics); labels.forEach({~it => message debug unit + "/" + it.y, <no project>, <no throwable>; int xTranslated = (int) (it.x * xKoef); int yTranslated = chartHeight - (int) ((it.y - minTemp) * yKoef); g2.setStroke(new BasicStroke(1)); if (prevX > 0) { // It is first element, no need to draw trailing line g2.drawLine(prevX, prevY, xTranslated, yTranslated); } g2.drawString(String.format("%.2f", it.y) + unit, xTranslated + 3, chartHeight - Math.abs(chartHeight - (yTranslated + 20))); g2.setStroke(new BasicStroke(5)); g2.drawLine(xTranslated, yTranslated, xTranslated, yTranslated); prevX = xTranslated; prevY = yTranslated; }); } }); } }; panel.setPreferredSize(new Dimension(chartWidth, chartHeight)); return panel; }
Первое, что бросается в глаза —
editorContext.getRepository().getModelAccess().runReadAction...
Это такая фишка редактора MPS: чтобы получить доступ к модели/узлу откуда угодно, нам нужно запросить выполнение этого кода. Это похоже наrunOnUIThread
в андроиде, смысл примерно тот же. Короче, если нужно получить что-то из главного потока, то нужно делать это именно так. Еще естьrunWriteAction
, он нужен для внесения изменений и он нам еще потребуется.
Что происходит внутри:
1) Мы определяем единицы измерения
1) Определяем ширину и высоту графика
2) Трансформируем массив типа WeatherTimedData в список типа java.awt.geom.Point2D.Double, гдеа y = температура в выбранном измерении, например, в цельсиях.
Мы используем синтаксис baseLanguage, который облегчает работу с коллекциями и позволяет нормально использовать различные паттерны, например map, filter, flatMap. Естественно,
вместо привычных названий используются select, where, selectMany соотвественно.
Внимание! Кусок кода, отвечающий за фильтрацию WeatherTimedData, а именноwhere({~it => !it.temperature.concept.isAbstract(); })
— когда мы инициализируем новый WeatherTimedData, то у нас не иницилизирована температура. То есть у нас нет дефолта в цельсиях или фаренгейтах, поэтому у нас абстрактная температура, и если бы мы не добавили этой фильтрации, то у нас зависал бы редактор. Вот он, опыт!
3) Получаем верхнюю и нижнюю границы температур, затем получаем те самые "коэффициенты" для проекций на оси
4) Рисование на компоненте — очень простая часть. Если рисуем первую точку — рисуем только точку и подпись о температуре, если рисуем НЕ первую — рисуем линию между предыдущей и текущей точками. Ну и плюс всякие визуальные прикольчики, аля отступы от краев, чтобы видно было текст.
Вау! Это что такое — реально график? Прямо в редакторе кода? Который реактивно обновляется если поменять температуру или время? Вау!
Тем не менее, сейчас у нас захардкожены ширина и высота графика, а так же мы не можем выбрать единицы измерения.
Самое время сейчас заменить везде наши захардкоженные "°C", "°F" на enumeration datatype. Думаю, объяснять суть enumeration не стоит, только в контексте MPS.
enumeration datatype — это простой enum class, который может быть использован в property.
Если раньше мы использовали только string, integer и _FPNumber_String, то теперь мы можем создать enum для единиц измерения температур, в котором будет 2 элемента: цельсий и фаренгейт.
ПКМ на WeatherPrediction.structure -> New -> Enum Data Type -> TemperatureUnit.
Выбираем тип, в данном случае string
Нам нужно дефолтное значение, так что оставляем false в no default
default = first member(celsius)
member identifier — отвечает за определение элемента по входным данным. Чтобы изменить значение TemperatureUnit, нужно подать на вход строку, которая сравнивается с каждым внутренним или внешним значением, смотря какое выбрать.
Поясняю: то, что слева и синенькое — внутренее значением элемента enum. Оно скрыто. Справа — внешнее, оно используется для отображения в редакторе.
То есть если мы в member identifier выберем derive from internal value, то задавать значение нам придется либо celsius, либо fahrenheit. А если мы выберем derive from presentation, то задавать значение придется строками °C или °F. Еще можно добавить кастомную идентификацию, например, чтобы можно было задавать значение по внутреннему и внешнему значению, но это уже сами, нам не нужно.
Выбираем derive from presentation и добавляем 2 элемента.
Четко!
Добавляем свойство unit в PredictionResult.
Теперь нужно добавить выпадающий список, в котором мы будем выбирать единицу измерения.string[] units = enum/TemperatureUnit/.members.select({~it => it.externalValue; }).toArray; final ModelAccess modelAccess = editorContext.getRepository().getModelAccess(); final JComboBox<string> box = new JComboBox<string>(units); box.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent p0) { modelAccess.executeCommand(new EditorCommand(editorContext) { protected void doExecute() { Object selectedItem = box.getSelectedItem(); node.unit = selectedItem.toString(); } }); } }); box.setSelectedIndex(0); box;
Это код для другого $swing component$ в коде редактора PredictionResult. Мы получаем список возможных единиц измерения температуры, создаем выпадающий список, вешаем обработчик события. Здесь тоже используется "прикол MPS", вместо readAction или writeAction можно просто executeCommand. Видимо, 2 предыдущих существуют для читаемости.
При изменении выбранного элемента из JComboBox меняется node.unit, который задается строковым значением, как я объяснял выше.
Собираем язык, смотрим.
Можете мне поверить, там действитетельно выпадает еще и фаренгейт. Осталось только связать JComboBox и график, и на этом можно будет закончить, а сделать это будет легко.
Привожу оригинальный код отрисовки графика.Захардкоженные единицы измерения{ public void run() { string unit = "°C"; final list<Point2D.Double> labels = node.input.items.select({~it => double x = it.time.hours * 60 + it.time.minutes; double y = it.temperature.getValueFromUnit(unit.toString()); new Point2D.Double(x, y); }).sortBy({~it => it.x; }, asc).toList; final double minTemp = labels.sortBy({~it => it.y; }, asc).first.y; final double maxTemp = labels.sortBy({~it => it.y; }, asc).last.y; final double yKoef = chartHeight / (maxTemp - minTemp); final double xKoef = chartWidth / (60.0 * 24.0); int prevY = chartHeight; int prevX = -1; Graphics2D g2 = ((Graphics2D) graphics); labels.forEach({~it => message debug unit + "/" + it.y, <no project>, <no throwable>; int xTranslated = (int) (it.x * xKoef); int yTranslated = chartHeight - (int) ((it.y - minTemp) * yKoef); g2.setStroke(new BasicStroke(1)); if (prevX > 0) { // It is first element, no need to draw trailing line g2.drawLine(prevX, prevY, xTranslated, yTranslated); } g2.drawString(String.format("%.2f", it.y) + unit, xTranslated + 3, chartHeight - Math.abs(chartHeight - (yTranslated + 20))); g2.setStroke(new BasicStroke(5)); g2.drawLine(xTranslated, yTranslated, xTranslated, yTranslated); prevX = xTranslated; prevY = yTranslated; }); } }
Да, смекаете? Нам нужно только заменить
string unit = "°C";
наstring unit = node.unit;
и мы гучи!
А теперь итог: график в цельсиях и фаренгейтах, уаа!
P.S.
Я думаю именно в этой статье очень много опечаток, расхождений, потому что я много отвлекался, как минимум на то, чтобы реализовать то, что хотел поведать в этой статье. Что ни день, то открытие, поэтому, пожалуйста, пишите в комментах все моменты, которые вам кажутся странными, скорее всего это я выпал из контекста повествования и написал какую-то ересь.
В следующей статье мы рассмотрим такой аспект, как TextGen. Будем генерировать прогноз погоды в текстовую форму!
Автор: enchantinggg