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

в 21:35, , рубрики: java, jetbrains, language design, mps, programming, Программирование

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

У меня на дачке не было света 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. формулы это ужас

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

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

    $x=hours * 60 + minutes$

    а y = температура в выбранном измерении, например, в цельсиях.
    Мы используем синтаксис baseLanguage, который облегчает работу с коллекциями и позволяет нормально использовать различные паттерны, например map, filter, flatMap. Естественно,
    вместо привычных названий используются select, where, selectMany соотвественно.
    Внимание! Кусок кода, отвечающий за фильтрацию WeatherTimedData, а именно where({~it => !it.temperature.concept.isAbstract(); }) — когда мы инициализируем новый WeatherTimedData, то у нас не иницилизирована температура. То есть у нас нет дефолта в цельсиях или фаренгейтах, поэтому у нас абстрактная температура, и если бы мы не добавили этой фильтрации, то у нас зависал бы редактор. Вот он, опыт!
    3) Получаем верхнюю и нижнюю границы температур, затем получаем те самые "коэффициенты" для проекций на оси
    4) Рисование на компоненте — очень простая часть. Если рисуем первую точку — рисуем только точку и подпись о температуре, если рисуем НЕ первую — рисуем линию между предыдущей и текущей точками. Ну и плюс всякие визуальные прикольчики, аля отступы от краев, чтобы видно было текст.
    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. Будем генерировать прогноз погоды в текстовую форму!

Автор: enchantinggg

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js