WME E95

Setup road properties with templates

Install this script?
Author's suggested script

You may also like WME E50 Fetch POI Data.

Install this script
// ==UserScript==
// @name         WME E95
// @name:uk      WME 🇺🇦 E95
// @version      0.8.1
// @description  Setup road properties with templates
// @description:uk Швидке налаштування атрибутів вулиці за шаблонами
// @license      MIT License
// @author       Anton Shevchuk
// @namespace    https://greasyfork.org/users/227648-anton-shevchuk
// @supportURL   https://github.com/AntonShevchuk/wme-e95/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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
// @grant        none
// @require      https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1218867/WME-Bootstrap.js
// @require      https://update.greasyfork.org/scripts/452563/1218878/WME.js
// @require      https://update.greasyfork.org/scripts/450221/1137043/WME-Base.js
// @require      https://update.greasyfork.org/scripts/450320/1281847/WME-UI.js
// ==/UserScript==

/* jshint esversion: 8 */
/* global require */
/* global $, jQuery */
/* global W */
/* global I18n */
/* global WME, WMEBase, WMEUI, WMEUIHelper, WMEUIShortcut, WMEUIHelperControlButton */
/* global Container, Settings, SimpleCache, Tools  */

(function () {
  'use strict'

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

  // Translations
  const TRANSLATION = {
    'en': {
      title: 'Quick Properties',
      description: 'Apply the road\'s settings by one click',
      help: 'You can use the <a href="#keyboard-dialog" target="_blank" rel="noopener noreferrer" data-toggle="modal">Keyboard shortcuts</a> to apply the settings. It\'s more convenient than clicking on the buttons.',
    },
    'uk': {
      title: 'Швидкі налаштування',
      description: 'Застосовуйте швидкі налаштування для доріг за один клік',
      help: 'Використовуйте <a href="#keyboard-dialog" target="_blank" rel="noopener noreferrer" data-toggle="modal">гарячі клавіши</a>, це значно швидше ніж використовувати кнопку',

    },
    'ru': {
      title: 'Быстрые настройки',
      description: 'Применяйте быстрые настройки для дорог в один клик',
      help: 'Используйте <a href="#keyboard-dialog" target="_blank" rel="noopener noreferrer" data-toggle="modal">комбинации клавиш</a>, и не надо будет клацать кнопку',
    }
  }

  const STYLE = 'button.waze-btn.E95 { margin: 0 4px 4px 0; padding: 2px; width: 42px; } ' +
    'button.waze-btn.E95:hover { 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.E95-E { margin-right: 42px; }' +
    'button.waze-btn.E95-J { margin-right: 42px; }' +
    'p.e95-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)

  // Road Types
  //   I18n.translations.uk.segment.road_types
  //   I18n.translations.en.segment.road_types
  const TYPES = {
    street: 1,
    primary: 2,
    freeway: 3,
    ramp: 4,
    trail: 5,
    major: 6,
    minor: 7,
    offroad: 8,
    walkway: 9,
    boardwalk: 10,
    ferry: 15,
    stairway: 16,
    private: 17,
    railroad: 18,
    runway: 19,
    parking: 20,
    narrow: 22
  }
  // Road colors by type
  const COLORS = {
    '1': '#ffffeb',
    '2': '#f0ea58',
    // ...
    '8': '#867342',
    // ...
    '17': '#beba6c',
    // ...
    '20': '#ababab'
  }
  // Road Flags
  //   for setup flags use binary operators
  //   e.g. flags.tunnel | flags.headlights
  const FLAGS = {
    tunnel: 0b00000001,
    // ???
    // a     : 0b00000010,
    // b     : 0b00000100,
    // c     : 0b00001000,
    unpaved: 0b00010000,
    headlights: 0b00100000,
  }
  // Buttons:
  //   title - for buttons
  //   shortcut - keys for shortcuts, by default is Alt + (1..9)
  //   options:
  //    - detectCity - try to detect the city name by closures segments
  //    - clearCity - clear the city name
  //    - clearStreet - clear the street name
  //   attributes - native settings for model object
  // TODO:
  //   – check permissions for user level lower than 2
  const BUTTONS = {
    A: {
      title: 'PR 5',
      shortcut: 'A+1',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 5,
        revMaxSpeed: 5,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.private,
        lockRank: 0,
      }
    },
    B: {
      title: 'PR20',
      shortcut: 'A+2',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 20,
        revMaxSpeed: 20,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.private,
        lockRank: 0,
      }
    },
    C: {
      title: 'PR50',
      shortcut: 'A+3',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 50,
        revMaxSpeed: 50,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.private,
        lockRank: 0,
      }
    },
    D: {
      title: 'St50',
      shortcut: 'A+4',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 50,
        revMaxSpeed: 50,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.street,
        lockRank: 0,
      }
    },
    E: {
      title: 'PS50',
      shortcut: 'A+5',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 50,
        revMaxSpeed: 50,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.primary,
        lockRank: 1,
      }
    },
    F: {
      title: 'PLR',
      shortcut: 'A+6',
      options: {
        detectCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 5,
        revMaxSpeed: 5,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.parking,
        lockRank: 0,
      }
    },
    G: {
      title: 'OR',
      shortcut: 'A+7',
      options: {
        clearCity: true,
        clearStreet: false,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 90,
        revMaxSpeed: 90,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.offroad,
        lockRank: 0,
      }
    },
    H: {
      title: 'PR90',
      shortcut: 'A+8',
      options: {
        clearCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 90,
        revMaxSpeed: 90,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.private,
        lockRank: 0,
      }
    },
    I: {
      title: 'St90',
      shortcut: 'A+9',
      options: {
        clearCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 90,
        revMaxSpeed: 90,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.street,
        lockRank: 0,
      }
    },
    J: {
      title: 'PS90',
      shortcut: 'A+0',
      options: {
        clearCity: true,
      },
      attributes: {
        flags: 0,
        fwdMaxSpeed: 90,
        revMaxSpeed: 90,
        fwdMaxSpeedUnverified: false,
        revMaxSpeedUnverified: false,
        roadType: TYPES.primary,
        lockRank: 1,
      }
    }
  }

  // codes of countries
  const COUNTRIES = {
    ukraine: 232
  }

  // country specified buttons config
  const CONFIGS = {
    // Ukraine
    232: {
      G: {
        attributes: {
          flags: FLAGS.headlights
        }
      },
      H: {
        attributes: {
          flags: FLAGS.headlights
        }
      },
      I: {
        attributes: {
          flags: FLAGS.headlights
        }
      },
      J: {
        attributes: {
          flags: FLAGS.headlights
        }
      },
    }
  }

  // Require Waze API
  let WazeActionUpdateObject
  let WazeActionUpdateFeatureAddress

  class E95 extends WMEBase {
    constructor (name, buttons, config  ) {
      super(name)

      this.helper = new WMEUIHelper(name)

      this.panel = null
      this.buttons = buttons
      this.config = config


      let tab = this.helper.createTab(
        I18n.t(name).title,
        {
          image: GM_info.script.icon
        }
      )
      tab.addText('description', I18n.t(name).description)
      tab.addDiv('text', I18n.t(name).help)
      tab.addText(
        'info',
        '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
      )
      tab.inject()
    }

    getPanel () {
      if (this.panel) {
        return this.panel
      }

      // Build panel
      // Container for buttons
      let controls = document.createElement('div')
      controls.className = 'controls'
      // Create buttons
      for (let btn in this.buttons) {
        let config = this.getButtonConfig(btn)
        let title = config.title
        let color = COLORS[config.attributes.roadType]
        let description = config.title + ' - ' +
          I18n.t('segment.road_types')[config.attributes.roadType] + '; ' +
          I18n.t('edit.segment.fields.speed_limit') + ' ' +
          I18n.t('measurements.speed.km', { speed: config.attributes.fwdMaxSpeed })

        let UIButton = new WMEUIHelperControlButton(
          NAME,
          btn,
          title,
          description,
          () => this.buttonCallback(config),
          config.shortcut
        )
        let button = UIButton.html()
        button.dataset[NAME] = btn
        button.style.backgroundColor = color
        controls.appendChild(button)
      }
      let label = document.createElement('label')
      label.className = 'control-label'
      label.innerText = I18n.t(NAME).title

      this.panel = document.createElement('div')
      this.panel.className = 'form-group ' + NAME
      this.panel.appendChild(label)
      this.panel.appendChild(controls)

      return this.panel
    }

    // Get Button settings
    getButtonConfig (index) {
      // Load settings for current country by call method W.model.getTopCountry().getID()
      // Then mixed it with default settings by Tools.mergeDeep() method
      return Tools.mergeDeep(this.buttons[index], this.config[index])
    }

    // Handler for Road buttons
    buttonCallback (button) {
      // Get all selected segments
      let segments = WME.getSelectedSegments()

      // Try to detect city, if needed
      if (button.options.detectCity) {
        let cityName = null
        for (let i = 0, total = segments.length; i < total; i++) {
          cityName = this.detectCity(segments[i])
          if (cityName) {
            button.options.cityName = cityName
            break
          }
        }
      }
      for (let i = 0, total = segments.length; i < total; i++) {
        this.updateSegment(segments[i], button.options, button.attributes)
      }
    }

    /**
     * Update segment attributes
     * @param {Object} segment
     * @param {Object} options
     * @param {Object} attributes
     */
    updateSegment (segment, options, attributes = {}) {
      // current segment address
      let addr = segment.getAddress()

      // fill address information
      let address = {
        countryID: addr.getCountry()?.getID() || W.model.getTopCountry().getID(),
        stateID: addr.getState()?.getID() || W.model.getTopState().getID(),
        cityName: addr.getCity()?.getName() || '',
        streetName: addr.getStreet()?.getName() || '',
      }
      // options: detect city
      if (!address.cityName && options.detectCity && options.cityName) {
        this.log('detected city name "' + options.cityName + '"')
        address.cityName = options.cityName
      }
      // options: clear city
      if (options.clearCity) {
        this.log('clear city name')
        address.cityName = null
      }
      // options: clear street
      if (options.clearStreet) {
        this.log('clear street name')
        address.streetName = null
      }
      // set city flag
      address.emptyCity = (address.cityName === null)
      // set street flag
      address.emptyStreet = (address.streetName === null) || (address.streetName === '')


      let updateFeatureAddress = new WazeActionUpdateFeatureAddress(
          segment,
          address,
          {
            streetIDField: 'primaryStreetID'
          }
      )

      // update segment's address
      W.model.actionManager.add(updateFeatureAddress)

      // keep the current lock level if it is higher than in the config's attributes
      if (segment.attributes.lockRank > attributes.lockRank) {
        attributes.lockRank = segment.attributes.lockRank
      }
      // need more logs
      this.log('set road type to ' + I18n.t('segment.road_types')[attributes.roadType])

      // update segment's properties
      let updateObject = new WazeActionUpdateObject(segment, attributes)

      W.model.actionManager.add(updateObject)
    }

    /**
     * Detect city name by connected segments
     * @param {Object} segment
     * @return {String|null}
     */
    detectCity (segment) {
      // check cityName of the segment
      if (segment.getAddress().getCity() && !segment.getAddress().getCity().isEmpty()) {
        return segment.getAddress().getCity().getName()
      }

      // TODO: replace follow magic with methods getConnectedSegments() and getConnectedSegmentsByDirection()
      //  W.selectionManager.getSelectedDataModelObjects()[0].getConnectedSegments().map(x => x.attributes.id)
      //  W.selectionManager.getSelectedDataModelObjects()[0].getConnectedSegmentsByDirection().map(x => x.attributes.id)
      //  when it will work
      //  last check - 2023.11.20

      // connected segments
      let connected = []
      connected = connected.concat(segment.getFromNode().getSegmentIds()) // segments from point A
      connected = connected.concat(segment.getToNode().getSegmentIds()) // segments from point B
      connected = connected.filter(id => id !== segment.getID()) // filter himself

      // cities of the connected segments
      let cities = connected.map(id => W.model.segments.getObjectById(id).getAddress().getCity())
      cities = cities.filter(city => city) // filter segments w/out city
      cities = cities.map(city => city.getName()) // extract cities name
      cities = cities.filter(city => city) // filter empty city name

      if (cities.length) {
        return cities.shift()
      }
      return null
    }

    /**
     * Handler for `segment.wme` event
     * Create UI controls every time when updated DOM of sidebar
     * Uses native JS function for better performance
     *
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {W.model} model
     * @return {void}
     */
    onSegment (event, element, model) {
      // Skip for walking trails and blocked roads
      if (model.isWalkingRoadType()
        || model.isLockedByHigherRank()
        || !model.isGeometryEditable()
      ) {
        return
      }

      // Panel can be already exists
      element.querySelector('div.form-group.E95') ||
      element.prepend(this.getPanel())
    }

    /**
     * Handler for `segments.wme` event
     * Create UI controls every time when updated DOM of sidebar
     * Uses native JS function for better performance
     *
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {Array} models
     * @return {void}
     */
    onSegments (event, element, models) {
      // Skip for walking trails or locked roads
      if (models.filter((model) => model.isWalkingRoadType() || model.isLockedByHigherRank() || !model.isGeometryEditable()).length > 0) {
        element.querySelector('div.form-group.E95')?.remove()
        return
      }
      // Panel can be already exists
      element.querySelector('div.form-group.E95') ||
      element.prepend(this.getPanel())
    }
  }

  $(document).on('bootstrap.wme', () => {
    // Require scripts
    WazeActionUpdateObject = require('Waze/Action/UpdateObject')
    WazeActionUpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress')

    // check country configuration
    let country = W.model.getTopCountry()?.getID() || COUNTRIES.ukraine
    let config = CONFIGS[country] ? CONFIGS[country] : CONFIGS[COUNTRIES.ukraine]

    new E95(NAME, BUTTONS, config)

    WMEUIShortcut.setGroupTitle(NAME, I18n.t(NAME).title)
  })
})()