WME E85 Simplify Street Geometry

Simplify Street Geometry, looks like fork

当前为 2025-11-25 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WME E85 Simplify Street Geometry
// @name:uk      WME 🇺🇦 E85 Simplify Street Geometry
// @name:ru      WME 🇺🇦 E85 Simplify Street Geometry
// @version      0.3.1
// @description  Simplify Street Geometry, looks like fork
// @description:uk Спрощуємо та вирівнюємо геометрію вулиць
// @description:ru Упрощаем и выравниваем геометрию улиц
// @license      MIT License
// @author       Anton Shevchuk
// @namespace    https://greasyfork.org/users/227648-anton-shevchuk
// @supportURL   https://github.com/AntonShevchuk/wme-e85/issues
// @match        https://*.waze.com/editor*
// @match        https://*.waze.com/*/editor*
// @exclude      https://*.waze.com/user/editor*
// @icon         data:image/png;base64,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cmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo3MDc5ZWFhZi0yYmFiLTQyYWQtODVkMi00MDQ0MWEyMjdmZTkiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDU4ODQ0OGYtZTI5OS00MTY2LTk2ZmQtY2Y1MDI1ZGFmM2JmIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjdlZDgzZjItYTQ3OC00MjI1LTlkOTYtNjYwZGUyMzk5ZmFhIgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iTWFjIE9TIgogICBHSU1QOlRpbWVTdGFtcD0iMTY3MDg0NjMzNzI5MjY2NyIKICAgR0lNUDpWZXJzaW9uPSIyLjEwLjMyIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMjoxMjoxMlQxMjo1ODo1NiswMTowMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjI6MTI6MTJUMTI6NTg6NTYrMDE6MDAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplZDRiNjEzOS1iYmU3LTQ3MzgtODZkYi1kZTYwMzk2OWM3NjciCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTWFjIE9TKSIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMi0xMi0xMlQxMjo1ODo1NyswMTowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4PVOYgAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5gwMCzo59Ra5FgAABz9JREFUeNrtmntMVFcexz+8QXQE1GEGmFVR44NHLXEayqPBWLdpt6nUB1oXLbQ10G1KbZvQVLtAXItNY//QbBraVB7bVNpx1bGGTReycbUzhAUJ0YLSAiXKY4b3Y2GFheHuH8iFy/CYZbWyw/0mk8zvnN95/H73nN/ve869IEOGDBkyZMiQIUOGjIUIB/FfGo448DsgDohCmFBnH5YKgAHQ4cCnZDAyXpmOmnSMuYW5gqnDJNgrTB0mIbcwVyAdI+moJzrAUFZdJiwUlFWXCaRjGF0Y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
// @grant        none
// @require      https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1691572/WME-Bootstrap.js
// @require      https://update.greasyfork.org/scripts/450221/1691071/WME-Base.js
// @require      https://update.greasyfork.org/scripts/450320/1688694/WME-UI.js
// ==/UserScript==

/* jshint esversion: 8 */

/* global require */
/* global $, jQuery */
/* global I18n */
/* global WMEBase */
/* global WMEUI, WMEUIHelper, WMEUIHelperPanel, WMEUIHelperModal, WMEUIHelperTab */
/* global Container, Settings, SimpleCache, Tools  */
/* global Node$1, Segment, Venue, VenueAddress, WmeSDK */

(function () {
  'use strict'

  // Script name, uses as unique index
  const NAME = 'E85'

  // Translations
  const TRANSLATION = {
    'en': {
      title: 'Street Geometry',
      description: 'Simplify and straighten up streets',
      buttons: {
        A: 'Simplify',
        B: 'Straighten',
      },
      settings: {
        simplify: {
          title: 'Settings',
          description: 'Settings for simplifying segments',
          short: 'Remove a fragment shorter than',
          angle: 'If the angle is bigger than',
          twoShort: 'and fragments shorter than',
        },
        buttons:{
          title: 'Buttons',
          description: 'Set the angle of the buttons',
          C: '1st Button',
          D: '2nd Button',
          E: '3rd Button',
          F: '4th Button',
          G: '5th Button',
        }
      },
    },
    'uk': {
      title: 'Геометрія вулиць',
      description: 'Спрощуйте та вирівнюйте вулиці',
      buttons: {
        A: 'Спростити',
        B: 'Вирівняти',
      },
      settings: {
        simplify: {
          title: 'Налаштування',
          description: 'Для спрощення сегментів будуть враховані наступні параметри',
          short: 'Видаляти фрагменти менші ніж',
          angle: 'Або якщо кут більше ніж',
          twoShort: 'та фрагменти меньші ніж',
        },
        buttons: {
          title: 'Кнопки',
          description: 'Налаштуйте кут для кнопок',
          C: 'Для першої',
          D: 'Для другої',
          E: 'Для третьої',
          F: 'Для четвертої',
          G: 'Для п\'ятої',
        }
      },
    },
    'ru': {
      title: 'Геометрия улиц',
      description: 'Упрощайте и выравнивайте геометрию улиц',
      buttons: {
        A: 'Упростить',
        B: 'Выровнять',
      },
      settings: {
        simplify: {
          title: 'Настройки',
          description: 'Параметры для упрощения геометрии сегмента',
          short: 'Если фрагмент короче, чем',
          angle: 'Или угол больше чем',
          twoShort: 'и фрагменты меньше, чем',
        },
        buttons: {
          title: 'Кнопки',
          description: 'Настройте угол для кнопок',
          C: 'Для 1-ой кнопки',
          D: 'Для 2-ой кнопки',
          E: 'Для 3-ей кнопки',
          F: 'Для 4-ой кнопки',
          G: 'Для 5-ой кнопки',
        }
      },
    }
  }

  WMEUI.addTranslation(NAME, TRANSLATION)

  const STYLE =
    'button.e85.e85-A { background-color: #0f9; margin-right: 2px }' +
    'button.e85.e85-B { background-color: #09f; margin-right: 20px; color: #fff }' +
    'button.e85.e85-C { background-color: #fdd; margin: 2px 2px 0 0}' +
    'button.e85.e85-D { background-color: #fbb; margin: 2px 2px 0 0 }' +
    'button.e85.e85-E { background-color: #f99; margin: 2px 2px 0 0 }' +
    'button.e85.e85-F { background-color: #f77; margin: 2px 2px 0 0 }' +
    'button.e85.e85-A:disabled, button.e85.e85-B:disabled { background-color: #ccc }' +
    '.e85 legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
    '.e85 fieldset { border: 1px solid #ddd; padding: 8px; }' +
    '.e85 fieldset.e85 div.controls label { white-space: normal; font-weight: normal; line-height: 32px; font-size: 13px; }' +
    '.e85 fieldset.e85 div.controls input[type="number"] { float:right; wight: 32px }' +
    'p.e85-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }' +
    '#sidebar p.e85-blue { background-color:#0057B8;color:white;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }' +
    '#sidebar p.e85-yellow { background-color:#FFDD00;color:black;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }'

  WMEUI.addStyle(STYLE)

  const BUTTONS = {
    A: {
      title: I18n.t(NAME).buttons.A,
      description: I18n.t(NAME).buttons.A,
      shortcut: null,
    },
    B: {
      title: I18n.t(NAME).buttons.B,
      description: I18n.t(NAME).buttons.B,
      shortcut: null,
    },
  }

  // Default settings
  const SETTINGS = {
    simplify: {
      short: 5,
      angle: 176,
      twoShort: 50,
    },
    buttons: {
      C: 180,
      D: 90,
      E: 60,
      F: 30
    }
  }

  class E85 extends WMEBase {

    constructor (name, settings, buttons) {
      super(name, settings)

      this.buttons = buttons

      this.initHelper()

      this.initTab()

      this.initShortcuts()
    }

    initHelper() {
      /** @type {WMEUIHelper} */
      this.helper = new WMEUIHelper(this.name)
    }

    /**
     * Initial UI elements
     */
    initTab () {
      /** @type {WMEUIHelperTab} */
      let tab = this.helper.createTab(
        I18n.t(this.name).title,
        {
          sidebar: this.wmeSDK.Sidebar,
          image: GM_info.script.icon
        }
      )

      // Setup options for the script
      let fieldset = this.helper.createFieldset(I18n.t(NAME).settings.simplify.title)
      fieldset.addText('description', I18n.t(NAME).settings.simplify.description)

      let simplify = this.settings.get('simplify')
      for (let item in simplify) {
        if (simplify.hasOwnProperty(item)) {
          fieldset.addNumber(
            'settings-simplify-' + item,
            I18n.t(NAME).settings.simplify[item],
            event => this.settings.set(['simplify', item], event.target.value),
            this.settings.get('simplify', item),
            (item === 'angle') ? 150 : 0,
            (item === 'angle') ? 180 : 200,
            1
          )
        }
      }

      tab.addElement(fieldset)

      // Setup options for the script
      let fieldsetButtons = this.helper.createFieldset(I18n.t(NAME).settings.buttons.title)
      fieldsetButtons.addText('description', I18n.t(NAME).settings.buttons.description)

      let settingsButtons = this.settings.get('buttons')
      for (let item in settingsButtons) {
        if (settingsButtons.hasOwnProperty(item)) {
          fieldsetButtons.addNumber(
            'settings-buttons-' + item,
            I18n.t(NAME).settings.buttons[item],
            event => this.settings.set(['buttons', item], event.target.value),
            this.settings.get('buttons', item),
            10,
            180,
            (item === 'F') ? 1 : 5
          )
        }
      }

      tab.addElement(fieldsetButtons)

      tab.addText(
        'info',
        '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
      )

      tab.addText('blue', 'made in')
      tab.addText('yellow', 'Ukraine')

      // Inject custom HTML to container in the WME interface
      tab.inject()
    }

    /**
     * Initial shortcuts
     */
    initShortcuts () {
      let shortcuts = [
        {
          description: I18n.t(NAME).description,
          shortcutId: this.id,
          shortcutKeys: 'A+E',
          callback: () => this.simplifySelected()
        },
        {
          description: I18n.t(NAME).description + ' [*]',
          shortcutId: this.id + '-all',
          shortcutKeys: 'A+R',
          callback: () => this.simplifyAll()
        },
      ]

      for (let shortcut of shortcuts) {
        if (this.wmeSDK.Shortcuts.areShortcutKeysInUse({ shortcutKeys: shortcut.shortcutKeys })) {
          this.log('Shortcut already in use')
          shortcut.shortcutKeys = null
        }
        this.wmeSDK.Shortcuts.createShortcut(shortcut);
      }
    }

    /**
     * Handler for `segment.wme` event
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {Segment} model
     * @return {void}
     */
    onSegment (event, element, model) {
      // Skip for blocked roads
      if (
        this.wmeSDK.DataModel.Segments.hasPermissions({ segmentId: model.id })
      ) {
        // Panel can be already exists
        let panel = this.helper.createPanel(I18n.t(this.name).title)

        let simplifyButton = panel.addButton(
          'A',
          BUTTONS.A.title,
          BUTTONS.A.description,
          () => this.simplifySegmentGeometry(model),
        )

        let straightenButton = panel.addButton(
          'B',
          BUTTONS.B.title,
          BUTTONS.B.description,
          () => this.straightenSegmentGeometry(model),
        )

        if (model.geometry.coordinates.length < 3) {
          simplifyButton.html().disabled = true
          straightenButton.html().disabled = true
        }

        const existingFormGroup = element.querySelector('div.form-group.e85');
        if (existingFormGroup) {
          existingFormGroup.replaceWith(panel.html());
        } else {
          element.prepend(panel.html());
        }
      } else {
        // Remove the panel
        element.querySelector('div.form-group.e85')?.remove()
      }
    }

    /**
     * Handler for `segments.wme` event
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {Array<Segment>} models
     * @return {void}
     */
    onSegments (event, element, models) {
      // Skip for locked roads
      if (models.filter((model) =>
          this.wmeSDK.DataModel.Segments.isRoadTypeDrivable({ roadType: model.roadType })
          && this.wmeSDK.DataModel.Segments.hasPermissions({ segmentId: model.id })
        ).length === 0
      ) {
        element.querySelector('div.form-group.e85')?.remove()
        return
      }

      let panel = this.helper.createPanel(I18n.t(this.name).title)
      let simplifyButton = panel.addButton(
        'A',
        BUTTONS.A.title,
        BUTTONS.A.description,
        () => this.simplifyStreetGeometry(models)
      )

      // Don't straighten multiple components
      let straightenButton = panel.addButton(
        'B',
        BUTTONS.B.title,
        BUTTONS.B.description,
        () => this.straightenStreetGeometry(models)
      )

      let modelWithComponents = models.filter(model => model.geometry.coordinates.length > 2)
      if (modelWithComponents.length === 0) {
        simplifyButton.html().disabled = true
      }

      if (models.length === 2) {
        let first = models[0]
        let second = models[1]

        // check connections of the first segment
        // trying to find second one
        let connections = []
        connections = connections.concat(this.wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId: first.id }))
        connections = connections.concat(this.wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId: first.id, reverseDirection: true }))
        connections = connections.map(segment => segment.id)

        if (connections.indexOf(second.id) !== -1) {
          panel.addDiv('align-by-angle')

          for (let key of ['C','D','E','F']) {
            let angle = this.settings.get('buttons', key)
            panel.addButton(
              key,
              `∡${angle}°`,
              `∡${angle}°`,
              () => this.alignStreetGeometry(first, second, angle),
              ''
            )
          }
        }
      }

      const existingFormGroup = element.querySelector('div.form-group.e85');
      if (existingFormGroup) {
        existingFormGroup.replaceWith(panel.html());
      } else {
        element.prepend(panel.html());
      }

      /*
      // @todo: need to check how it works with new SDK
      if (W.selectionManager.getSegmentSelection().multipleConnectedComponents) {
        straightenButton.html().disabled = true
      }
      */
    }

    /**
     * Remove geometry nodes on the target segment
     * @param {Segment} model
     * @return {void}
     */
    simplifySegmentGeometry (model) {
      this.log('check geometry of the segment with ID ' + model.id)

      if (model.geometry.coordinates.length < 3) {
        this.log('geometry is simple, skipped')
        return
      }

      this.group('simplify segment geometry')
      let nodes = []

      // calculate angles for every inside point
      for (let i = 0; i < model.geometry.coordinates.length - 2; i++) {
        let nodeStart = model.geometry.coordinates[i],
          nodeCenter = model.geometry.coordinates[i + 1],
          nodeEnd = model.geometry.coordinates[i + 2]

        nodes[i] = {
          angle: Math.round(GeoUtils.findAngle(nodeStart, nodeCenter, nodeEnd)),
          start: Math.round(GeoUtils.getDistance(nodeStart, nodeCenter)),
          end: Math.round(GeoUtils.getDistance(nodeCenter, nodeEnd)),
        }
        this.log('point ' + (i+1) + ' : ' + nodes[i].angle + '°, ' + nodes[i].start + 'm, ' + nodes[i].end + 'm')
      }

      let removeNodes = []

      for (let i = 0; i < nodes.length; i++) {
        let node = nodes[i]

        // mark to remove a node with a short START segment
        if (node.start < this.settings.get('simplify', 'short')) {
          this.log('found too short segment: ' + node.start + 'm; point: ' + (i + 1))
          removeNodes.push(i+1)
          continue // skip the next rule
        }
        // mark to remove a node with a short END segment and big ANGLE
        if (node.angle >= this.settings.get('simplify', 'angle')
          && node.end < this.settings.get('simplify', 'short')) {
          this.log('found too short fragment: ' + node.end + 'm; point: ' + (i + 1))
          removeNodes.push(i+1)
          i++ // skip next node
          continue // skip the next rule
        }
        // mark to remove a node with a big angle and short segments
        if (node.angle >= this.settings.get('simplify', 'angle')
          && node.start + node.end < this.settings.get('simplify', 'twoShort')) {
          this.log(
            'found point with short fragment: ' + node.start + ' + ' + node.end + ' = ' +
            (node.start + node.end) + 'm and angle equal to ' + node.angle + '°'
          )
          removeNodes.push(i+1)
          // continue // skip next rule
        }
      }


      // remove nodes from geometry
      if (removeNodes.length) {
        this.log('points to remove: ' + removeNodes.join(', '))
        let geometry = structuredClone(model.geometry)
        geometry.coordinates = []
        // insert coordinates if it shouldn't delete
        for (let i = 0; i < model.geometry.coordinates.length; i++) {
          if (removeNodes.indexOf(i) === -1) {
            this.log(
              'Add point ' + (i + 1) + ' : '
              + model.geometry.coordinates[i][0] + ','
              + model.geometry.coordinates[i][1]
            )
            geometry.coordinates.push(model.geometry.coordinates[i])
          }
        }

        this.wmeSDK.DataModel.Segments.updateSegment({
          segmentId: model.id, geometry
        })
      } else {
        this.log('modification is not needed')
      }
      this.groupEnd()
    }

    /**
     * Remove geometry nodes on all segments on the screen
     * @return {void}
     */
    simplifyAll () {
      this.group('simplify on screen segments')

      let segments = this.getAllSegments()

      segments = segments.filter((model) =>
        this.wmeSDK.DataModel.Segments.isRoadTypeDrivable({ roadType: model.roadType })
        && this.wmeSDK.DataModel.Segments.hasPermissions({ segmentId: model.id })
      )

      this.simplifyStreetGeometry(
        segments
      )
      this.groupEnd()
    }

    /**
     * Remove geometry nodes on the selected segments
     * @return {void}
     */
    simplifySelected () {
      this.group('simplify selected segments')
      this.simplifyStreetGeometry(
        this.getSelectedSegments()
      )
      this.groupEnd()
    }

    /**
     * Remove geometry nodes on the target segments
     * @param {Array<Segment>} models
     * @return {void}
     */
    simplifyStreetGeometry (models) {
      this.group('simplify street geometry')
      for (let i = 0; i < models.length; i++) {
        this.simplifySegmentGeometry(models[i])
      }
      this.groupEnd()
    }

    /**
     * Aligns the segments into a straight line by moving the intermediate
     * nodes to the intersection points of the perpendiculars with
     * the calculated line passing through the start and end nodes of the selection.
     *
     * A, B, and C are the parameters of the calculated line equation:
     *   Ax + By + C = 0
     *
     * @param {Array<Segment>} models
     * @return {void}
     */
    straightenStreetGeometry (models) {
      this.group('straighten street geometry')

      /**
       * Find intersection point
       * @param {Number} A
       * @param {Number} B
       * @param {Number} C
       * @param {Number} D
       * @return {Number[]}
       */
      function getIntersectCoordinates (A, B, C, D) {

        let r = [2]
        r[1] = -1.0 * (C * B - A * D) / (A * A + B * B)
        r[0] = (-r[1] * (B + A) - C + D) / (A - B)

        return r
      }

      this.log('calculating the formula for the straight line')

      /*
      // @todo: need to check how it works with new SDK
      let segmentSelection = W.selectionManager.getSegmentSelection()

      if (segmentSelection.multipleConnectedComponents) {
        this.log('don\'t try to straighten multiple segments without connection')
      }
       */

      let
        allNodeIds = [], // all nodes for selected segments
        dupNodeIds = [], // only nodes inside connections
        virtualNodes = [] // virtual nodes of segments

      models.forEach(segment => {
        this.log('straighten segment #' + segment.id)

        // collect the nodes
        allNodeIds.push(segment.fromNodeId)
        allNodeIds.push(segment.toNodeId)

        let nodes = this.wmeSDK.DataModel.Segments.getVirtualNodes( {segmentId: segment.id} )

        if (nodes.length === 0) {
          // simplify a segment to straight
          this.straightenSegmentGeometry(segment)
        } else {
          // don't do anything if we have virtual nodes
          virtualNodes = virtualNodes.concat(nodes)
        }
      })

      if (virtualNodes.length ) {
        this.log('⚠️ virtual nodes are present, please disconnect all trails and rails from the segments and try again')
        this.groupEnd()
        // doesn't work, but why? what is wrong with this code?
        // virtualNodes.forEach(node => {
        //   let element = document.getElementById(node.getOLGeometry.id)
        //   element.setAttribute("fill","#dd7700")
        //
        //   element.addEventListener("click", () => {
        //     element.setAttribute("fill","#00ece3")
        //   });
        // })

        return
      }

      allNodeIds.forEach((nodeId, idx) => {
        if (allNodeIds.indexOf(nodeId, idx + 1) > -1) {
          if (!dupNodeIds.includes(nodeId))
            dupNodeIds.push(nodeId);
        }
      });

      let distinctNodeIds = [...new Set(allNodeIds)];
      let endPointNodeIds = distinctNodeIds.filter((nodeId) => !dupNodeIds.includes(nodeId));
      let endPointNodes = endPointNodeIds.map(id => this.wmeSDK.DataModel.Nodes.getById( {nodeId: id} )),
        endPointNode1Geo = endPointNodes[0].geometry.coordinates,
        endPointNode2Geo = endPointNodes[1].geometry.coordinates

      const a = endPointNode2Geo[1] - endPointNode1Geo[1],
        b = endPointNode1Geo[0] - endPointNode2Geo[0],
        c = endPointNode2Geo[0] * endPointNode1Geo[1] - endPointNode1Geo[0] * endPointNode2Geo[1];

      dupNodeIds.forEach((nodeId) => {
        const node = this.wmeSDK.DataModel.Nodes.getById( {nodeId: nodeId} )
        const nodeCoordinates = node.geometry.coordinates
        const d = nodeCoordinates[1] * a - nodeCoordinates[0] * b
        const newCoordinates = getIntersectCoordinates(a, b, c, d);
        const newGeometry = node.geometry
        newGeometry.coordinates = newCoordinates

        this.log('move node #' + nodeId + ' to [' + newCoordinates[0] + ';' + newCoordinates[1] + ']')

        this.wmeSDK.DataModel.Nodes.moveNode({
          id: nodeId,
          geometry: newGeometry
        })
      });

      // I don't understand why doesn't it work, in the WME all looks good, but it fails when try to save changes
      // virtualNodes.forEach((node) => {
      //   const nodeCoordinates = node.getGeometry().coordinates;
      //   const d = nodeCoordinates[1] * a - nodeCoordinates[0] * b,
      //     newCoordinates = getIntersectCoordinates(a, b, c, d);
      //
      //   this.log('move node #' + node.getID() + ' to [' + newCoordinates[0] + ';' + newCoordinates[1] + ']')
      //   this.moveNode(node, newCoordinates)
      // });

      this.groupEnd()
    }


    /**
     * Align two segments by angle
     * This method moves the node to new point
     *
     * @param {Segment} first
     * @param {Segment} second
     * @param {Number} angle
     * @return {void}
     */
    alignStreetGeometry (first, second, angle = 90) {
      this.log('align street geometry ∡' + angle + '°')

      /**
       * Extract coordinates from components
       * @param {Segment} segment
       * @param {'first'|'second'|'next-to-last'|'last'} position
       * @return {*[]}
       */
      function getCoordinatesFromComponent(segment, position) {
        let pos = 0
        switch (position) {
          case 'first':
            pos = 0
            break
          case 'second':
            pos = 1
            break
          case 'next-to-last':
            pos = segment.geometry.coordinates.length - 2
            break
          case 'last':
            pos = segment.geometry.coordinates.length - 1
            break
        }
        return [
          segment.geometry.coordinates[pos][0],
          segment.geometry.coordinates[pos][1],
        ]
      }

      let A, B, C, commonNode

      if (first.toNodeId === second.fromNodeId) {
        // A → B → C
        this.log('A → B → C')
        commonNode = this.wmeSDK.DataModel.Nodes.getById( { nodeId: first.toNodeId } )

        A = getCoordinatesFromComponent(first, 'next-to-last')
        B = getCoordinatesFromComponent(first, 'last')
        C = getCoordinatesFromComponent(second, 'second')
      } else if (first.fromNodeId === second.fromNodeId) {
        // B ← A → C
        this.log('B ← A → C')
        commonNode = this.wmeSDK.DataModel.Nodes.getById( { nodeId: first.fromNodeId } )

        A = getCoordinatesFromComponent(first, 'second')
        B = getCoordinatesFromComponent(first, 'first')
        C = getCoordinatesFromComponent(second, 'second')
      } else if (first.toNodeId === second.toNodeId) {
        // A → B ← C
        this.log('A → B ← C')
        commonNode = this.wmeSDK.DataModel.Nodes.getById( { nodeId: first.toNodeId } )

        A = getCoordinatesFromComponent(first, 'next-to-last')
        B = getCoordinatesFromComponent(first, 'last')
        C = getCoordinatesFromComponent(second, 'next-to-last')
      } else if (first.fromNodeId === second.toNodeId) {
        // B ← A ← C
        this.log('B ← A ← C')
        commonNode = this.wmeSDK.DataModel.Nodes.getById( { nodeId: first.fromNodeId } )

        A = getCoordinatesFromComponent(first, 'second')
        B = getCoordinatesFromComponent(first, 'first')
        C = getCoordinatesFromComponent(second, 'next-to-last')
      }

      if (!commonNode) {
        this.log('segments does not have common node')
        return
      }

      this.log('common node coords [' + B[0] + ';' + B[1] + ']')
      this.log('current angle is ' + GeoUtils.findAngle(A, B, C) + '°')

      let intersection

      // For 180
      if (parseInt(angle) === 180) {
        // Check current angle
        let current = GeoUtils.findAngle(A, B, C)

        if (180 === Math.round(current)) {
          this.log('current angle is already ~180°, skipped')
          return
        }

        // Find the distance
        let distAB = GeoUtils.getAngularDistance(A, B)
        let distBC = GeoUtils.getAngularDistance(B, C)
        let distAC = GeoUtils.getAngularDistance(A, C)

        let distance
        if (distAC < distAB && distAC < distBC) {
          distance = distAC / 2
        } else if (distAB < distBC) {
          distance = distAB
        } else {
          distance = distBC
        }

        let bearing = GeoUtils.getBearing(A, C)

        intersection = GeoUtils.getDestination(A, bearing, distance);
      } else {
        // Find the intersection
        intersection = GeoUtils.findIntersection(A, B, C, angle);
      }

      if (!intersection) {
        this.log('intersection not found')
        return
      }

      this.log('point of the intersection is [' + intersection[0] + ', ' + intersection[1] +']')
      this.log('new angle is ' + GeoUtils.findAngle(A, intersection, C) + '°')

      commonNode.geometry.coordinates = intersection

      this.wmeSDK.DataModel.Nodes.moveNode({
        id: commonNode.id,
        geometry: commonNode.geometry
      })
    }

    /**
     * Straighten up segment, remove all geometry nodes except first and last
     * @param {Segment} segment
     */
    straightenSegmentGeometry (segment) {
      this.log('straighten segment geometry')
      if (segment.geometry.coordinates.length > 2) {
        let geometry = structuredClone(segment.geometry)
        // remove all coordinates except first and last
        geometry.coordinates.splice(
          1, geometry.coordinates.length - 2
        )
        this.wmeSDK.DataModel.Segments.updateSegment({
          segmentId: segment.id,
          geometry: geometry
        })
      }
    }
  }

  /**
   * A utility class for spherical geometry (geodesy).
   * Assumes points are [longitude, latitude] in degrees.
   */
  class GeoUtils {
    /**
     * @param {number} degrees
     * @return {number} radians
     * @private
     */
    static _toRadians(degrees) {
      return degrees * (Math.PI / 180);
    }

    /**
     * @param {number} radians
     * @return {number} degrees
     * @private
     */
    static _toDegrees(radians) {
      return radians * (180 / Math.PI);
    }

    /**
     * Normalizes an angle to the range -180 to +180 degrees.
     * @param {number} degrees
     * @return {number} degrees
     */
    static _normalizeAngle(degrees) {
      return (degrees + 540) % 360 - 180;
    }

    /**
     * Calculates the initial bearing from pA to pB.
     * @param {[number,number]} pA - [lon, lat] of start point.
     * @param {[number,number]} pB - [lon, lat] of end point.
     * @returns {number} Initial bearing in degrees (0-360).
     */
    static getBearing(pA, pB) {
      const latA = GeoUtils._toRadians(pA[1]);
      const lonA = GeoUtils._toRadians(pA[0]);
      const latB = GeoUtils._toRadians(pB[1]);
      const lonB = GeoUtils._toRadians(pB[0]);

      const deltaLon = lonB - lonA;

      const y = Math.sin(deltaLon) * Math.cos(latB);
      const x = Math.cos(latA) * Math.sin(latB) -
        Math.sin(latA) * Math.cos(latB) * Math.cos(deltaLon);

      const bearingRad = Math.atan2(y, x);

      // Convert from -180/+180 to 0-360
      return (GeoUtils._toDegrees(bearingRad) + 360) % 360;
    }

    /**
     * Calculates the interior angle at vertex p2.
     * @param {[number,number]} p1
     * @param {[number,number]} p2
     * @param {[number,number]} p3
     */
    static findAngle(p1, p2, p3) {
      const bearing21 = GeoUtils.getBearing(p2, p1);
      const bearing23 = GeoUtils.getBearing(p2, p3);
      let angle = Math.abs(bearing21 - bearing23);

      if (angle > 180) {
        angle = 360 - angle;
      }
      return angle;
    }

    /**
     * Calculate the approximate distance between two coordinates (lat/lon)
     *
     * @param {[number,number]} pA - [lon, lat] of start point.
     * @param {[number,number]} pB - [lon, lat] of end point.
     * @return {number} The distance in meters.
     */
    static getDistance (pA, pB) {
      return GeoUtils.getAngularDistance(pA, pB) * 6371000
    }

    /**
     * Calculates the angular distance between two points using the Haversine formula.
     * @param {[number,number]} pA - [lon, lat] of start point.
     * @param {[number,number]} pB - [lon, lat] of end point.
     * @returns {number} The angular distance in radians.
     */
    static getAngularDistance(pA, pB) {
      const latA = GeoUtils._toRadians(pA[1]);
      const lonA = GeoUtils._toRadians(pA[0]);
      const latB = GeoUtils._toRadians(pB[1]);
      const lonB = GeoUtils._toRadians(pB[0]);

      const deltaLat = latB - latA;
      const deltaLon = lonB - lonA;

      const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
        Math.cos(latA) * Math.cos(latB) *
        Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);

      // c is the angular distance in radians
      return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    }

    /**
     * Calculates the destination point given a start point, bearing, and distance.
     * @param {[number,number]} startPoint - [lon, lat] of start point.
     * @param {number} bearing - Bearing in degrees (0-360).
     * @param {number} distanceRad - Angular distance in radians.
     * @returns {number[]} The destination point [lon, lat] in degrees.
     */
    static getDestination(startPoint, bearing, distanceRad) {
      const lat1 = GeoUtils._toRadians(startPoint[1]);
      const lon1 = GeoUtils._toRadians(startPoint[0]);
      const brng = GeoUtils._toRadians(bearing);
      const d = distanceRad;

      const lat2 = Math.asin(
        Math.sin(lat1) * Math.cos(d) +
        Math.cos(lat1) * Math.sin(d) * Math.cos(brng)
      );

      const lon2 = lon1 + Math.atan2(
        Math.sin(brng) * Math.sin(d) * Math.cos(lat1),
        Math.cos(d) - Math.sin(lat1) * Math.sin(lat2)
      );

      // Normalize longitude to -180 to +180
      const lon2Deg = GeoUtils._toDegrees(lon2);
      const lat2Deg = GeoUtils._toDegrees(lat2);

      return [(lon2Deg + 540) % 360 - 180, lat2Deg];
    }

    /**
     * Finds the intersection of two great-circle paths.
     * Path 1: Defined by p1 and p2.
     * Path 2: Defined by p3 and an internal angle at p3.
     *
     * @param {[number,number]} p1 - First point of Line 1 [lon, lat].
     * @param {[number,number]} p2 - Second point of Line 1 [lon, lat].
     * @param {[number,number]} p3 - Start point of Line 2 [lon, lat].
     * @param {number} angle - The SIGNED internal angle at p2 (in degrees).
     * @returns {[number,number] | null} The intersection point [lon, lat], or null if lines are parallel.
     */
    static findIntersection(p1, p2, p3, angle) {
      // 1. Define the triangle P1-P3-X (A-C-B)
      //    A = p1, C = p3, B = X (intersection)

      // 2. Find known bearings
      const brng1_2 = GeoUtils.getBearing(p1, p2); // Bearing of Line 1
      const brng1_3 = GeoUtils.getBearing(p1, p3); // Bearing from p1 to p3

      // 3. Calculate internal angles A (at p1) and C (at p3)
      const angleA = GeoUtils._normalizeAngle(brng1_2 - brng1_3);
      const angleB = angle
      const angleC = GeoUtils._normalizeAngle(180 - angleA - angleB)

      // 4. Calculate known side b (angular distance p1-p3)
      const dist_b = GeoUtils.getAngularDistance(p1, p3); // in radians

      // Check for parallel lines
      if (Math.sin(GeoUtils._toRadians(angleA)) === 0 && Math.sin(GeoUtils._toRadians(angleC)) === 0) {
        return null; // Collinear
      }

      // 5. Find internal angle B (at intersection X) using Law of Cosines for angles
      const angleA_rad = GeoUtils._toRadians(angleA);
      const angleB_rad = GeoUtils._toRadians(angleB);
      const angleC_rad = GeoUtils._toRadians(angleC);

      // 5a. Find internal angle B (at intersection X) using Law of Cosines for angles
      let cos_B = -Math.cos(angleA_rad) * Math.cos(angleC_rad) +
        Math.sin(angleA_rad) * Math.sin(angleC_rad) * Math.cos(dist_b);
      cos_B = Math.max(-1, Math.min(1, cos_B)); // Clamp
      // angleB_rad = Math.acos(cos_B);

      // Check for parallel/collinear lines (angleB is 0 or 180)
      if (Math.abs(Math.sin(angleB_rad)) < 1e-9) {
        return null;
      }

      // 5b. Find side c (distance p1-X) using Law of Cosines (NOT Sines)
      //     cos(c) = (cos(C) + cos(A)cos(B)) / (sin(A)sin(B))
      let cos_c = (Math.cos(angleC_rad) + Math.cos(angleA_rad) * cos_B) /
        (Math.sin(angleA_rad) * Math.sin(angleB_rad));

      cos_c = Math.max(-1, Math.min(1, cos_c)); // Clamp
      const dist_c = Math.acos(cos_c); // This is dist_p1_X in radians

      // 6. Calculate the final intersection point X
      //    We have start (p1), bearing (brng1_X), and distance (dist_c)
      return GeoUtils.getDestination(p1, brng1_2, dist_c);
    }
  }

  $(document).on('bootstrap.wme', () => {
    new E85(NAME, SETTINGS, BUTTONS)
  })
})()