WME E87 Inconsistent direction

Solves the inconsistent direction problem

目前為 2022-12-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WME E87 Inconsistent direction
// @version      0.0.1
// @description  Solves the inconsistent direction problem
// @license      MIT License
// @author       Anton Shevchuk
// @namespace    https://greasyfork.org/users/227648-anton-shevchuk
// @supportURL   https://github.com/AntonShevchuk/wme-template/issues
// @match        https://*.waze.com/editor*
// @match        https://*.waze.com/*/editor*
// @exclude      https://*.waze.com/user/editor*
// @icon         https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://anton.shevchuk.name&size=64
// @grant        none
// @require      https://greasyfork.org/scripts/389765-common-utils/code/CommonUtils.js?version=1090053
// @require      https://greasyfork.org/scripts/450160-wme-bootstrap/code/WME-Bootstrap.js?version=1128320
// @require      https://greasyfork.org/scripts/452563-wme/code/WME.js?version=1101598
// @require      https://greasyfork.org/scripts/450221-wme-base/code/WME-Base.js?version=1129908
// @require      https://greasyfork.org/scripts/450320-wme-ui/code/WME-UI.js?version=1128560
// ==/UserScript==

/* jshint esversion: 8 */

/* global require */
/* global $, jQuery */
/* global W */
/* global I18n */
/* global OpenLayers */
/* global WME, WMEBase */
/* global WMEUI, WMEUIHelper, WMEUIHelperPanel, WMEUIHelperModal, WMEUIHelperTab, WMEUIShortcut, WMEUIHelperFieldset */
/* global Container, Settings, SimpleCache, Tools  */

(function () {
  'use strict'

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

  // Translations
  const TRANSLATION = {
    'en': {
      title: 'Direction',
      description: 'Plugin WME E87 solves the inconsistent direction problem',
      buttons: {
        A: 'A → B',
        B: 'B → A',
      },
    },
    'uk': {
      title: 'Напрямки →',
      description: 'Плагін WME E87 для вирішиння проблеми різно направленних вулиць',
      buttons: {
        A: 'A → B',
        B: 'B → A',
      },
    },
    'ru': {
      title: 'Направления →',
      description: 'Плагин WME E87 для решения проблемы разнонаправленных улиц',
      buttons: {
        A: 'A → B',
        B: 'B → A',
      },
    }
  }

  const STYLE =
    'button.waze-btn.e87 { background: #f2f4f7; border: 1px solid #ccc; margin: 2px 8px; } ' +
    'button.waze-btn.e87:hover { background: #ffffff; transition: background-color 100ms linear; box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1), inset 0 0 100px 100px rgba(255, 255, 255, 0.3); } ' +
    'button.waze-btn.e87:focus { background: #f2f4f7; } ' +
    'div.e87-container { display: flex; } ' +
    'p.e87-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }'

  WMEUI.addTranslation(NAME, TRANSLATION)
  WMEUI.addStyle(STYLE)

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

  // Default settings
  const SETTINGS = {}

  class E87 extends WMEBase {
    /**
     * Initial UI elements
     * @param {Object} buttons
     */
    init (buttons) {
      /** @type {WMEUIHelper} */
      this.helper = new WMEUIHelper(this.name)

      /** @type {WMEUIHelperTab} */
      this.tab = this.helper.createTab(
        I18n.t(this.name).title,
        I18n.t(this.name).description,
        {
          'icon': '<i class="w-icon panel-header-component-icon w-icon-route"></i>'
        }
      )

      buttons.A.callback = () => this.onButtonA()
      buttons.B.callback = () => this.onButtonB()

      this.tab.addButtons(buttons)

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

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

    onButtonA () {
      this.log('Button A')
    }

    onButtonB () {
      this.log('Button B')
    }

    /**
     * Handler for `segment.wme` event
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {W.model} model
     * @return {void}
     */
    onSegment (event, element, model) {
      this.log('Selected one segment')
    }

    /**
     * Handler for `segments.wme` event
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {Array} models
     * @return {void}
     */
    onSegments (event, element, models) {
      this.log('Check selected segments')
      this.detect(models)
    }

    /**
     * Detect directions
     * @param {Array<Object>} segments
     */
    detect (segments) {
      let forward = [], backward = [], node
      for (let i = 0; i < segments.length; i++) {
        // check first segment direction
        let A = segments[i].getFromNode().getID()
        let B = segments[i].getToNode().getID()
        let nextA, nextB

        // check next segment
        if (i + 1 < segments.length) {
          nextA = segments[i + 1].getFromNode().getID()
          nextB = segments[i + 1].getToNode().getID()
        } else {
          if (node === A) {
            forward.push(segments[i].getID())
          } else if (node === B) {
            backward.push(segments[i].getID())
          }
          continue
        }

        // looking for intersection of two segments
        if (A === nextA || A === nextB) {
          // B-A × A-B or B-A × B-A
          node = A
          backward.push(segments[i].getID())
        } else if (B === nextA || B === nextB) {
          // A-B × A-B or A-B × B-A
          node = B
          forward.push(segments[i].getID())
        } else {
          this.log('Segments doesn\'t have intersection')
          return
        }
      }

      if (forward.length && backward.length) {
        this.log('Inconsistent direction detected: forward = ' + forward.length + ' backward = ' + backward.length)

        let buttonToForward = document.createElement('button')
        buttonToForward.type = 'button'
        buttonToForward.className = 'waze-btn waze-btn-small waze-btn-white e87'
        buttonToForward.innerText = 'Make all forward (' + backward.length + ')'
        buttonToForward.onclick = (e) => {
          e.preventDefault()
          backward.forEach(el => this.invert(el))
        }
        let buttonToBackward = document.createElement('button')
        buttonToBackward.type = 'button'
        buttonToBackward.className = 'waze-btn waze-btn-small waze-btn-white e87'
        buttonToBackward.innerText = 'Make all backward (' + forward.length + ')'
        buttonToBackward.onclick = (e) => {
          e.preventDefault()
          forward.forEach(el => this.invert(el))
        }

        let container = document.createElement('div')
        container.className = 'e87-container'
        container.append(buttonToForward)
        container.append(buttonToBackward)

        $('wz-alert.sidebar-alert.inconsistent-direction-alert .sidebar-alert-content').after(container)
      }
    }

    /**
     * Invert direction of the segment
     * @param {Number} id of the segment
     */
    invert (id) {
      let segment = W.model.segments.getObjectById(id)
      if (segment.isLockedByHigherRank()) {
        this.log('Locked by higher rank')
        return
      }
      console.groupCollapsed(
        '%c' + this.name + ':%c invert segment',
        'color: #0DAD8D; font-weight: bold',
        'color: dimgray; font-weight: normal'
      )

      console.log('segment', segment)

      let attributes = {}
      attributes.fwdDirection = segment.attributes.revDirection
      attributes.revDirection = segment.attributes.fwdDirection
      // attributes.fwdTurnsLocked = segment.revTurnsLocked // ???
      // attributes.revTurnsLocked = segment.fwdTurnsLocked // ???
      attributes.fwdMaxSpeed = segment.attributes.revMaxSpeed
      attributes.revMaxSpeed = segment.attributes.fwdMaxSpeed
      attributes.fwdMaxSpeedUnverified = segment.attributes.revMaxSpeedUnverified
      attributes.revMaxSpeedUnverified = segment.attributes.fwdMaxSpeedUnverified
      attributes.fwdLaneCount = segment.attributes.revLaneCount
      attributes.revLaneCount = segment.attributes.fwdLaneCount

      attributes.restrictions = []

      for (let i = 0; i < segment.attributes.restrictions.length; i++) {
        attributes.restrictions[i] = segment.attributes.restrictions[i].withReverseDirection()
      }

      console.log('attributes', attributes)

      let fromNode = segment.getFromNode()
      let toNode = segment.getToNode()

      let onA = {}
      let toConnections = {}
      fromNode.getSegmentIds().forEach(segId => {
        if (segId !== id) {
          onA[segId] = W.model.getTurnGraph().getTurnThroughNode(fromNode, W.model.segments.getObjectById(segId), segment)
          onA[segId].toVertex.direction = onA[segId].toVertex.direction === 'fwd' ? 'rev' : 'fwd'
        }

        toConnections[segId] = W.model.getTurnGraph().getTurnThroughNode(fromNode, segment, W.model.segments.getObjectById(segId))
        toConnections[segId].fromVertex.direction = toConnections[segId].fromVertex.direction === 'fwd' ? 'rev' : 'fwd'

        // u-turn
        if (segId === id) {
          toConnections[segId].toVertex.direction = toConnections[segId].toVertex.direction === 'fwd' ? 'rev' : 'fwd'
        }
      })

      let onB = {}
      let fromConnections = {}
      toNode.getSegmentIds().forEach(segId => {
        if (segId !== id) {
          onB[segId] = W.model.getTurnGraph().getTurnThroughNode(toNode, W.model.segments.getObjectById(segId), segment)
          onB[segId].toVertex.direction = onB[segId].toVertex.direction === 'fwd' ? 'rev' : 'fwd'
        }

        fromConnections[segId] = W.model.getTurnGraph().getTurnThroughNode(toNode, segment, W.model.segments.getObjectById(segId))
        fromConnections[segId].fromVertex.direction = fromConnections[segId].fromVertex.direction === 'fwd' ? 'rev' : 'fwd'

        // u-turn
        if (segId === id) {
          fromConnections[segId].toVertex.direction = fromConnections[segId].toVertex.direction === 'fwd' ? 'rev' : 'fwd'
        }
      })

      // on inverse la geometrie du segment
      let geometry = segment.geometry.clone()
      geometry.components.reverse()

      // controle de position
      let nbPoints = geometry.components.length - 1
      if (!geometry.components[0].equals(toNode.attributes.geometry)) {
        let delta = { x: 0, y: 0 }
        delta.x = toNode.attributes.geometry.x - geometry.components[0].x
        delta.y = toNode.attributes.geometry.y - geometry.components[0].y
        geometry.components[0].move(delta.x, delta.y)
      }
      if (!geometry.components[nbPoints].equals(fromNode.attributes.geometry)) {
        let delta = { x: 0, y: 0 }
        delta.x = fromNode.attributes.geometry.x - geometry.components[nbPoints].x
        delta.y = fromNode.attributes.geometry.y - geometry.components[nbPoints].y
        geometry.components[nbPoints].move(delta.x, delta.y)
      }

      // On deconnecte le segment
      W.model.actionManager.add(new WazeActionMultiAction([new WazeActionDisconnectSegment(segment, fromNode), new WazeActionDisconnectSegment(segment, toNode)]))

      // maj de la geo du seg
      W.model.actionManager.add(new WazeActionUpdateSegmentGeometry(segment, segment.geometry, geometry))

      // On reconnecte le segment
      W.model.actionManager.add(new WazeActionMultiAction([new WazeActionConnectSegment(toNode, segment), new WazeActionConnectSegment(fromNode, segment)]))

      this.applyTurns(fromConnections)
      this.applyTurns(toConnections)
      this.applyTurns(onA)
      this.applyTurns(onB)

      console.groupEnd()
    }

    /**
     * Apply turns for segments
     * @param segments
     */
    applyTurns (segments) {
      let actions = []
      for (let sid in segments) {
        let segment = segments[sid]
        let turn
        switch (segment.turnData.state) {
          case 0 :
          case 1 :
            turn = WazeModelGraphTurnData.create()
            turn = turn.withState(segment.turnData.state)
            turn = turn.withRestrictions(segment.turnData.restrictions)
            turn = turn.withInstructionOpcode(segment.turnData.instructionOpcode)
            turn = turn.withLanes(segment.turnData.lanes)

            actions.push(new WazeModelGraphActionsSetTurn(W.model.getTurnGraph(), segment.withTurnData(turn)))
            break
        }
      }
      W.model.actionManager.add(new WazeActionMultiAction(actions))
    }
  }

  let WazeActionConnectSegment
  let WazeActionDisconnectSegment
  let WazeActionModifyAllConnections
  let WazeActionMultiAction
  let WazeActionUpdateObject
  let WazeActionUpdateSegmentGeometry
  let WazeModelGraphTurnData
  let WazeModelGraphActionsSetTurn

  $(document).on('bootstrap.wme', () => {
    let Instance = new E87(NAME, SETTINGS)
    Instance.init(BUTTONS)

    WazeActionConnectSegment = require('Waze/Action/ConnectSegment')
    WazeActionDisconnectSegment = require('Waze/Action/DisconnectSegment')
    WazeActionModifyAllConnections = require('Waze/Action/ModifyAllConnections')
    WazeActionMultiAction = require('Waze/Action/MultiAction')
    WazeActionUpdateObject = require('Waze/Action/UpdateObject')
    WazeActionUpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry')
    WazeModelGraphTurnData = require('Waze/Model/Graph/TurnData')
    WazeModelGraphActionsSetTurn = require('Waze/Model/Graph/Actions/SetTurn')
  })
})()