Pull to refresh

JetBrains MPS для интересующихся #3

Reading time 7 min
Views 4K

Бинго-бонго и Джимбо-джамбо, дорогие друзья!


У меня на дачке не было света 2 дня, я практически иссох и впал в спячку, но я снова здесь! В этом посте мы начнем писать предсказания погоды и немного напишем кода, а не потыкаем мышкой! Ура! Наконец-то!


Какие прогнозы мы хотим делать


Очень простые! Пока прогнозировать будем только следующий день, а правила придумаем сами; а точнее, правил не будет. Мы просто будем выводить температуру на следующий день, абсолютно такую же, как и сегодня. Сделаем один прикольчик, демонстрирующий возможности projectional editor.


Концепты


В данном случае мы прибегнем к крутой фиче — мы создадим концепт, который будет содержать только ссылку на исходные данные, а данные мы будем выводить как график на своем Swing компоненте. О как умеем, хотя swing я жуть как не люблю.


image


Создаем концепт PredictionResult, добавляем в него reference "input", которая является ссылкой на реализацию концепта в текущем scope AST! Но поскольку нам не нужны области видимости, или scope, то для нас сойдет поиск всех элементов данного типа.(Кстати, Scopes это не самая легкая тема в MPS + по ней довольно сложная документация, местами непонятная, так что я накатаю статейку про Scope тоже. Когда нибудь.) Но теперь нужно добавить идентификацию для WeatherData, изменим немного структуру и Editor аспект.


image

Я добавил INamedConcept после implements, и теперь у нашего концепта WeatherData есть имя, но мы его никак не присваеваем, поэтому изменим Editor.


image

Здесь мы просто добавили 1 строчку, которая будет содержать имя. Пересоберем язык и посмотрим, че получилось.


image

Ура, теперь называем эту WeatherData именем "today" и возвращаемся к концепту PredictionResult и меняем его Editor аспект.


image

Пока так. У нас будет отображаться Prediction for tommorow, data %name_of_weather_data%
Добавим концепт в PredictionList — наш рутовый концепт, где пока находятся только входные данные.


image


image


Если собрать, то получится


image

… как раз то, чего мы и хотели. Мы можем выбирать из списка WeatherData(ничего страшного, что у нас только 1 WeatherData, зато расширяемо).


Здорово, теперь нужно как-то круто выводить наши прогнозы. Я уже написал, что выводить мы их будем на swing компоненте, если кто не знает — javax.swing. — пакет для разработки нативных графических интерфейсов на Java. На нем построена IntelliJ. Swing компоненты можно юзать в editor. Уря.


Перед тем, как рисовать все это дело, распишем по пунктам, как мы будем действовать.


  1. Берем ширину графика в пикселях и делим ее на 60 * 24 — количество минут в дне. Это нужно для того, чтобы правильно отображать точки по оси абсцисс.


  2. Переводим все температуры в одну единицу измерения, например, цельсии (потом мы настроим так, чтобы можно было самим выбирать, в цельсиях показывать или в фаренгейтах) и находим наибольшую температуру и наименьшую. Вычитаем из наибольшей наименьшую и получаем полную "высоту" в градусах. Суть в том, что если мы поделим высоту графика на эту величину, то получим сколько "пикселей в одном градусе". Это потребуется для того, чтобы проецировать температуры на график.


  3. Сортируем массив входных данных по времени(чем ближе к 00:00 — тем меньше, естественно) и проходимся по нему. Вычисляем x по формуле


    $$display$$время_в_минутах * коэффициент_из_пункта_1$$display$$


    а y


    $$display$$температура * коэффициент__из__пункта 2$$display$$


    P.S. формулы это ужас


  4. Рисуем!

Чтобы Вас не мучать поэтапным написанием строчек, скину весь и пройдусь по более-менее сложным местам.


{ 
  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, где


$x = hours * 60 + minutes$


а y = температура в выбранном измерении, например, в цельсиях.


Мы используем синтаксис baseLanguage, который облегчает работу с коллекциями и позволяет нормально использовать различные паттерны, например map, filter, flatMap. Естественно,
вместо привычных названий используются select, where, selectMany соотвественно.


Внимание! Кусок кода, отвечающий за фильтрацию WeatherTimedData, а именно where({~it => !it.temperature.concept.isAbstract(); }) — когда мы инициализируем новый WeatherTimedData, то у нас не иницилизирована температура. То есть у нас нет дефолта в цельсиях или фаренгейтах, поэтому у нас абстрактная температура, и если бы мы не добавили этой фильтрации, то у нас зависал бы редактор. Вот он, опыт!


4) Получаем верхнюю и нижнюю границы температур, затем получаем те самые "коэффициенты" для проекций на оси
5) Рисование на компоненте — очень простая часть. Если рисуем первую точку — рисуем только точку и подпись о температуре, если рисуем НЕ первую — рисуем линию между предыдущей и текущей точками. Ну и плюс всякие визуальные прикольчики, аля отступы от краев, чтобы видно было текст.


image

Вау! Это что такое — реально график? Прямо в редакторе кода? Который реактивно обновляется если поменять температуру или время? Вау!


Тем не менее, сейчас у нас захардкожены ширина и высота графика, а так же мы не можем выбрать единицы измерения.


Самое время сейчас заменить везде наши захардкоженные "°C", "°F" на enumeration datatype. Думаю, объяснять суть enumeration не стоит, только в контексте MPS.


enumeration datatype — это простой enum class, который может быть использован в property.
Если раньше мы использовали только string, integer и _FPNumber_String, то теперь мы можем создать enum для единиц измерения температур, в котором будет 2 элемента: цельсий и фаренгейт.


ПКМ на WeatherPrediction.structure → New → Enum Data Type → TemperatureUnit.


image

Выбираем тип, в данном случае 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.


image


Теперь нужно добавить выпадающий список, в котором мы будем выбирать единицу измерения.


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, который задается строковым значением, как я объяснял выше.


Собираем язык, смотрим.


image


Можете мне поверить, там действитетельно выпадает еще и фаренгейт. Осталось только связать 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; и мы гучи!


А теперь итог: график в цельсиях и фаренгейтах, уаа!


image


image


P.S.
Я думаю именно в этой статье очень много опечаток, расхождений, потому что я много отвлекался, как минимум на то, чтобы реализовать то, что хотел поведать в этой статье. Что ни день, то открытие, поэтому, пожалуйста, пишите в комментах все моменты, которые вам кажутся странными, скорее всего это я выпал из контекста повествования и написал какую-то ересь.


В следующей статье мы рассмотрим такой аспект, как TextGen. Будем генерировать прогноз погоды в текстовую форму!

Tags:
Hubs:
+7
Comments 2
Comments Comments 2

Articles