Бинго-бонго и Джимбо-джамбо, дорогие друзья!
У меня на дачке не было света 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 по формуле
а y
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) Мы определяем единицы измерения
2) Определяем ширину и высоту графика
3) Трансформируем массив типа WeatherTimedData в список типа java.awt.geom.Point2D.Double, где
а y = температура в выбранном измерении, например, в цельсиях.
Мы используем синтаксис baseLanguage, который облегчает работу с коллекциями и позволяет нормально использовать различные паттерны, например map, filter, flatMap. Естественно,
вместо привычных названий используются select, where, selectMany соотвественно.
Внимание! Кусок кода, отвечающий за фильтрацию WeatherTimedData, а именно where({~it => !it.temperature.concept.isAbstract(); })
— когда мы инициализируем новый WeatherTimedData, то у нас не иницилизирована температура. То есть у нас нет дефолта в цельсиях или фаренгейтах, поэтому у нас абстрактная температура, и если бы мы не добавили этой фильтрации, то у нас зависал бы редактор. Вот он, опыт!
4) Получаем верхнюю и нижнюю границы температур, затем получаем те самые "коэффициенты" для проекций на оси
5) Рисование на компоненте — очень простая часть. Если рисуем первую точку — рисуем только точку и подпись о температуре, если рисуем НЕ первую — рисуем линию между предыдущей и текущей точками. Ну и плюс всякие визуальные прикольчики, аля отступы от краев, чтобы видно было текст.
Вау! Это что такое — реально график? Прямо в редакторе кода? Который реактивно обновляется если поменять температуру или время? Вау!
Тем не менее, сейчас у нас захардкожены ширина и высота графика, а так же мы не можем выбрать единицы измерения.
Самое время сейчас заменить везде наши захардкоженные "°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. Будем генерировать прогноз погоды в текстовую форму!
icepro
Сделайте пожалуйста оглавление. Имеется ввиду не к данной статье, а как раз к другим статьям. Я вот пропустил вторую часть и придется искать. А было бы удобно видеть в начале каждой статьи ссылки на другие части.