WME Address Point Helper

Creates point with same address

Från och med 2025-03-17. Se den senaste versionen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         WME Address Point Helper
// @description  Creates point with same address
// @version      2.5.4
// @license      MIT License
// @author       Andrei Pavlenko, Anton Shevchuk
// @namespace    https://greatest.deepsurf.us/ru/users/160654-waze-ukraine
// @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.greatest.deepsurf.us/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greatest.deepsurf.us/scripts/450160/1218867/WME-Bootstrap.js
// @require      https://update.greatest.deepsurf.us/scripts/452563/1218878/WME.js
// @require      https://update.greatest.deepsurf.us/scripts/450221/1137043/WME-Base.js
// @require      https://update.greatest.deepsurf.us/scripts/450320/1555192/WME-UI.js
// @require      https://update.greatest.deepsurf.us/scripts/480123/1281900/WME-EntryPoint.js

// ==/UserScript==

/* jshint esversion: 8 */
/* global require */
/* global GM_info */
/* global $, jQuery */
/* global W, W.model */
/* global I18n */
/* global OpenLayers */
/* global NavigationPoint */
/* global WME, WMEBase, WMEUI, WMEUIHelper */
/* global Container, Settings, SimpleCache, Tools  */

(function () {
  'use strict'

  // Script name, uses as unique index
  const NAME = 'ADDRESS-POINT-HELPER'

  const TRANSLATION = {
    'en': {
      title: 'APH📍',
      description: 'Address Point Helper 📍',
      buttons: {
        createPoint: 'Clone to POI',
        createResidential: 'Clone to AT',
        newPoint: 'Create new point'
      },
      settings: {
        title: 'Options',
        addNavigationPoint: 'Add entry point',
        inheritNavigationPoint: 'Inherit parent\'s landmark entry point',
        autoSetHNToName: 'Copy house number into name',
        noDuplicates: 'Do not create duplicates'
      }
    },
    'uk': {
      title: 'APH📍',
      description: 'Address Point Helper 📍',
      buttons: {
        createPoint: 'Клон до POI',
        createResidential: 'Клон до АТ',
        newPoint: 'Створити нову точку POI'
      },
      settings: {
        title: 'Налаштування',
        addNavigationPoint: 'Додавати точку в\'їзду',
        inheritNavigationPoint: 'Наслідувати точку в\'їзду від POI',
        autoSetHNToName: 'Копіювати номер будинку в назву',
        noDuplicates: 'Не створювати дублікатів'
      }
    },
    'ru': {
      title: 'APH📍',
      description: 'Address Point Helper 📍',
      buttons: {
        createPoint: 'Клон в POI',
        createResidential: 'Клон в АТ',
        newPoint: 'Создать новую точку POI'
      },
      settings: {
        title: 'Настройки',
        addNavigationPoint: 'Создавать точку въезда',
        inheritNavigationPoint: 'Наследовать точку въезда от POI',
        autoSetHNToName: 'Копировать номер дома в название',
        noDuplicates: 'Не создавать дубликатов'
      }
    }
  }

  const STYLE = '.address-point-helper legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
    '.address-point-helper fieldset { border: 1px solid #ddd; padding: 4px; }' +
    '.address-point-helper fieldset div.controls label { white-space: normal; }' +
    'button.address-point-helper { border: 1px solid #ddd; margin-right: 2px; }' +
    'p.address-point-helper-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)

  // Set shortcuts title
  WMEUIShortcut.setGroupTitle(NAME, I18n.t(NAME).description)

  // default settings
  const SETTINGS = {
    addNavigationPoint: false,
    inheritNavigationPoint: false,
    autoSetHNToName: false,
    noDuplicates: false
  }

  const BUTTONS = {
    A: {
      title: '<i class="w-icon w-icon-node"></i> ' + I18n.t(NAME).buttons.createPoint,
      description: I18n.t(NAME).buttons.createPoint,
      shortcut: 'A+G',
      callback: () => createPoint()
    },
    B: {
      title: '<i class="fa fa-map-marker"></i> ' + I18n.t(NAME).buttons.createResidential,
      description: I18n.t(NAME).buttons.createResidential,
      shortcut: 'A+H',
      callback: () => createResidential()
    },
  }

  let scriptSettings = new Settings(NAME, SETTINGS)

  class APH extends WMEBase {
    constructor (name, settings) {
      super(name, settings)

      this.helper = new WMEUIHelper(NAME)

      // Create tab for settings
      this.tab = this.helper.createTab(
        I18n.t(NAME).title,
        {
          icon: 'home'
        }
      )

      // Setup options
      let fieldsetSettings = this.helper.createFieldset(I18n.t(NAME).settings.title)

      for (let item in settings.container) {
        if (settings.container.hasOwnProperty(item)) {
          fieldsetSettings.addCheckbox(
            item,
            I18n.t(NAME).settings[item],
            event => settings.set([item], event.target.checked),
            settings.get(item)
          )
        }
      }
      this.tab.addElement(fieldsetSettings)

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

      this.tab.inject()

      // Create a panel for POI
      this.panel = this.helper.createPanel(I18n.t(NAME).title)
      this.panel.addButtons(BUTTONS)

      /* name, desc, group, title, shortcut, callback, scope */
      new WMEUIShortcut(
        this.name + '_new_point',
        I18n.t(NAME).buttons.newPoint,
        this.name,
        I18n.t(NAME).buttons.newPoint,
        '80', // P
        () => $('.toolbar-group-item.other').find('wz-button.point').click()
      ).register()
    }

    /**
     * Handler for `venue.wme` event
     * @param {jQuery.Event} event
     * @param {HTMLElement} element
     * @param {W.model} model
     * @return {null|void}
     */
    onVenue (event, element, model) {
      if (!model.isGeometryEditable()) {
        return
      }
      if (element.querySelector('div.form-group.address-point-helper')) {
        return
      }
      element.prepend(this.panel.html())

      $('button.address-point-helper-A').prop('disabled', !validateForPoint())
      $('button.address-point-helper-B').prop('disabled', !validateForResidential())
    }

    /**
     * Handler for window `beforeunload` event
     * @param {jQuery.Event} event
     * @return {Null}
     */
    onBeforeUnload (event) {
      this.settings.save()
    }
  }

  $(document).on('bootstrap.wme', () => {
    new APH(NAME, scriptSettings)

    // Register handler for changes
    registerEventListeners()
  })

  function createPoint (isResidential = false) {
    console.groupCollapsed(
      '%c' + NAME + ': 📍%c try to create ' + (isResidential ? 'residential ' : '') + 'point',
      'color: #0DAD8D; font-weight: bold',
      'color: dimgray; font-weight: normal'
    )

    if ((!validateForPoint() && !isResidential)
      || (!validateForResidential() && isResidential)) {
      console.log('Invalid point')
      console.groupEnd()
      return
    }

    let WazeFeatureVectorLandmark = require('Waze/Feature/Vector/Landmark')
    let WazeActionAddLandmark = require('Waze/Action/AddLandmark')
    let WazeActionUpdateObject = require('Waze/Action/UpdateObject')
    let WazeActionUpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress')

    let { lat, lon } = getPointCoordinates()
    let address = getSelectedLandmarkAddress()
    let lockRank = getPointLockRank()

    let pointGeometry = new OpenLayers.Geometry.Point(lon, lat)

    let NewPoint = new WazeFeatureVectorLandmark({
      geoJSONGeometry: W.userscripts.toGeoJSONGeometry(pointGeometry)
    })
    NewPoint.attributes.categories.push('OTHER')
    NewPoint.attributes.lockRank = lockRank
    NewPoint.attributes.residential = isResidential

    if (scriptSettings.get('addNavigationPoint')) {
      let newEntryPoint, parentEntryPoint = WME.getSelectedVenue().getAttributes().entryExitPoints[0]
      if (scriptSettings.get('inheritNavigationPoint') && parentEntryPoint !== undefined) {
        newEntryPoint = new entryPoint().with({primary: true, point: parentEntryPoint.getPoint()})
      } else {
        newEntryPoint = new entryPoint({primary: true, point: W.userscripts.toGeoJSONGeometry(pointGeometry.clone())})
      }
      NewPoint.attributes.entryExitPoints.push(newEntryPoint)
    }

    if (!!address.attributes.houseNumber) {
      NewPoint.attributes.name = address.attributes.houseNumber
      NewPoint.attributes.houseNumber = address.attributes.houseNumber
    }

    let newAddressAttributes = {
      streetName: address.getStreetName(),
      emptyStreet: false,
      stateID: address.getState().getID(),
      countryID: address.getCountry().getID(),
    }

    if (address.getCity().getID() === 55344
    || address.getCityName() === 'поза НП') {
      newAddressAttributes.cityName = ''
      newAddressAttributes.emptyCity = true
    } else {
      newAddressAttributes.cityName = address.getCityName()
      newAddressAttributes.emptyCity = false
    }

    if (scriptSettings.get('noDuplicates') && hasDuplicate(NewPoint, newAddressAttributes, isResidential)) {
      console.log('This point already exists.')
      console.groupEnd()
      return
    }

    W.selectionManager.unselectAll()
    let addedLandmark = new WazeActionAddLandmark(NewPoint)
    W.model.actionManager.add(addedLandmark)
    W.model.actionManager.add(new WazeActionUpdateFeatureAddress(NewPoint, newAddressAttributes))
    if (!!address.attributes.houseNumber) {
      W.model.actionManager.add(new WazeActionUpdateObject(NewPoint, { houseNumber: address.attributes.houseNumber }))
    }
    W.selectionManager.setSelectedModels([addedLandmark.venue])
    console.log('The point was created.')
    console.groupEnd()
  }

  function createResidential () {
    createPoint(true)
  }

  function validateForPoint () {
    if (!WME.getSelectedVenue()) return false
    let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
    return /\d+/.test(selectedPoiHN)
  }

  function validateForResidential () {
    if (!WME.getSelectedVenue()) return false
    let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
    return /^\d+[А-ЯЇІЄ]{0,3}$/i.test(selectedPoiHN)
  }

  function getSelectedLandmarkAddress () {
    return WME.getSelectedVenue().getAddress()
  }

  function getPointLockRank () {
    let selectedLandmark = WME.getSelectedVenue()
    let userRank = W.loginManager.user.attributes.rank
    let parentFeatureLockRank = selectedLandmark.getLockRank()

    if (userRank >= parentFeatureLockRank) {
      return parentFeatureLockRank
    } else if (userRank >= 1) {
      return 1
    } else {
      return 0
    }
  }

  function getPointCoordinates () {
    let selectedLandmark = WME.getSelectedVenue()
    let selectedLandmarkGeometry = selectedLandmark.getOLGeometry()

    let coords
    if (/polygon/i.test(selectedLandmarkGeometry.id)) {
      let polygonCenteroid = selectedLandmarkGeometry.components[0].getCentroid()
      let geometryComponents = selectedLandmarkGeometry.components[0].components
      let flatComponentsCoords = []
      geometryComponents.forEach(c => flatComponentsCoords.push(c.x, c.y))
      let interiorPoint = getInteriorPointOfArray(
        flatComponentsCoords,
        2, [polygonCenteroid.x, polygonCenteroid.y]
      )
      coords = {
        lon: interiorPoint[0],
        lat: interiorPoint[1]
      }
    } else {
      coords = {
        lon: selectedLandmarkGeometry.x,
        lat: selectedLandmarkGeometry.y
      }
    }

    coords.lon += 4 // shift by X
    coords.lat += 5 // shift by Y
    return coords
  }

  function hasDuplicate (poi, addr, isResidential) {
    const venues = W.model.venues.getObjectArray()

    for (let key in venues) {
      if (!venues.hasOwnProperty(key)) continue
      const currentVenue = venues[key]
      const currentAddress = currentVenue.getAddress()

      let equalNames = true // or empty for residential
      if (!isResidential && !!currentVenue.attributes.name && !!poi.attributes.name) {
        if (currentVenue.attributes.name !== poi.attributes.name) {
          equalNames = false
        }
      }
      if (
        equalNames
        && poi.attributes.houseNumber === currentVenue.attributes.houseNumber
        && poi.attributes.residential === currentVenue.attributes.residential
        && addr.streetName === currentAddress.getStreetName()
        && addr.cityName === currentAddress.getCityName()
        && addr.countryID === currentAddress.getCountry().getID()
      ) {
        return true
      }
    }
    return false
  }

  function registerEventListeners () {
    let WazeActionUpdateObject = require('Waze/Action/UpdateObject')

    W.model.actionManager.events.register('afteraction', null, action => {
      // Задаем номер дома в название, если нужно. Пока не нашел более лаконичного способа определить что
      // произошло именно изменение адреса. Можно тестить регуляркой поле _description, но будут проблемы с
      // нюансами содержания этого поля на разных языках
      if (scriptSettings.get('autoSetHNToName')) {
        try {
          let subAction = action.action.subActions[0]
          let houseNumber = subAction.attributes.houseNumber
          let feature = subAction.feature
          if (feature.attributes.categories.includes('OTHER') && feature.attributes.name === '') {
            W.model.actionManager.add(new WazeActionUpdateObject(feature, { name: houseNumber }))
          }

          $('button.address-point-helper-A').prop('disabled', !validateForPoint())
          $('button.address-point-helper-B').prop('disabled', !validateForResidential())

        } catch (e) { /* Do nothing */ }
      }
    })
  }

  /**
   * @link https://github.com/openlayers/openlayers
   */
  function getInteriorPointOfArray (flatCoordinates, stride, flatCenters) {
    let offset = 0
    let flatCentersOffset = 0
    let ends = [flatCoordinates.length]
    let i, ii, x, x1, x2, y1, y2
    const y = flatCenters[flatCentersOffset + 1]
    const intersections = []
    // Calculate intersections with the horizontal line
    for (let r = 0, rr = ends.length; r < rr; ++r) {
      const end = ends[r]
      x1 = flatCoordinates[end - stride]
      y1 = flatCoordinates[end - stride + 1]
      for (i = offset; i < end; i += stride) {
        x2 = flatCoordinates[i]
        y2 = flatCoordinates[i + 1]
        if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
          x = (y - y1) / (y2 - y1) * (x2 - x1) + x1
          intersections.push(x)
        }
        x1 = x2
        y1 = y2
      }
    }
    // Find the longest segment of the horizontal line that has its center point
    // inside the linear ring.
    let pointX = NaN
    let maxSegmentLength = -Infinity
    intersections.sort(numberSafeCompareFunction)
    x1 = intersections[0]
    for (i = 1, ii = intersections.length; i < ii; ++i) {
      x2 = intersections[i]
      const segmentLength = Math.abs(x2 - x1)
      if (segmentLength > maxSegmentLength) {
        x = (x1 + x2) / 2
        if (linearRingsContainsXY(flatCoordinates, offset, ends, stride, x, y)) {
          pointX = x
          maxSegmentLength = segmentLength
        }
      }
      x1 = x2
    }
    if (isNaN(pointX)) {
      // There is no horizontal line that has its center point inside the linear
      // ring.  Use the center of the the linear ring's extent.
      pointX = flatCenters[flatCentersOffset]
    }

    return [pointX, y, maxSegmentLength]
  }

  function numberSafeCompareFunction (a, b) {
    return a > b ? 1 : a < b ? -1 : 0
  }

  function linearRingContainsXY (flatCoordinates, offset, end, stride, x, y) {
    // http://geomalgorithms.com/a03-_inclusion.html
    // Copyright 2000 softSurfer, 2012 Dan Sunday
    // This code may be freely used and modified for any purpose
    // providing that this copyright notice is included with it.
    // SoftSurfer makes no warranty for this code, and cannot be held
    // liable for any real or imagined damage resulting from its use.
    // Users of this code must verify correctness for their application.
    let wn = 0
    let x1 = flatCoordinates[end - stride]
    let y1 = flatCoordinates[end - stride + 1]
    for (; offset < end; offset += stride) {
      const x2 = flatCoordinates[offset]
      const y2 = flatCoordinates[offset + 1]
      if (y1 <= y) {
        if (y2 > y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) > 0) {
          wn++
        }
      } else if (y2 <= y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) < 0) {
        wn--
      }
      x1 = x2
      y1 = y2
    }
    return wn !== 0
  }

  function linearRingsContainsXY (flatCoordinates, offset, ends, stride, x, y) {
    if (ends.length === 0) {
      return false
    }
    if (!linearRingContainsXY(flatCoordinates, offset, ends[0], stride, x, y)) {
      return false
    }
    for (let i = 1, ii = ends.length; i < ii; ++i) {
      if (linearRingContainsXY(flatCoordinates, ends[i - 1], ends[i], stride, x, y)) {
        return false
      }
    }
    return true
  }
})()