// ==UserScript==
// @name WME Address Point Helper
// @author Andrei Pavlenko
// @version 1.12.5
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @exclude https://www.waze.com/user/*editor/*
// @exclude https://www.waze.com/*/user/*editor/*
// @grant none
// @description Creates point with same address
// @namespace https://gf.qytechs.cn/users/182795
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// @require https://gf.qytechs.cn/scripts/16071-wme-keyboard-shortcuts/code/WME%20Keyboard%20Shortcuts.js
// ==/UserScript==
var locale
var settings = {
addNavigationPoint: false,
inheritNavigationPoint: false,
autoSetHNToName: false,
noDuplicates: false
}
var translations = {
'en': {
createPoint: 'Create POI',
createResidential: 'Create residential',
addEntryPoint: 'Add entry point',
inheritEntryPoint: 'Inherit parent\'s landmark entry point',
copyHNToName: 'Copy house number into name',
noDuplicates: 'Do not create duplicates'
},
'uk': {
createPoint: 'Створити POI точку',
createResidential: 'Створити АТ',
addEntryPoint: 'Додавати точку в\'їзду',
inheritEntryPoint: 'Наслідувати точку в\'їзду батьківського ПОІ',
copyHNToName: 'Копіювати номер будинку в назву',
noDuplicates: 'Не створювати дублікатів'
},
'ru': {
createPoint: 'Создать POI точку',
createResidential: 'Создать АТ',
addEntryPoint: 'Создавать точку въезда',
inheritEntryPoint: 'Наследовать точку въезда родительского ПОИ',
copyHNToName: 'Копировать номер дома в название',
noDuplicates: 'Не создавать дубликатов'
}
}
var hnValidators = {
'Ukraine': hn => {
let valid = false
try {
valid = /^\d+[А-ЯЇІЄ]{0,3}$/i.test(hn)
} catch (e) { /* Do nothing */ }
return valid
},
'default': hn => {
return /.+/.test(hn)
}
};
(function () {
setTimeout(init, 1000)
})()
function init () {
try {
if (
document.getElementById('sidebarContent') !== null &&
document.getElementById('user-tabs') !== null && WazeWrap.Ready
) {
initLocale()
createScriptTab()
initSettings()
registerKeyboardShortcuts()
registerEventListeners()
} else {
setTimeout(init, 1000)
return
}
} catch (err) {
setTimeout(1000, init)
return
}
}
function createScriptTab () {
const html = `
<div id="sidepanel-aph">
<p>WME Address Point Helper 📍</p>
<div class="controls-container"><input type="checkbox" id="aph-add-navigation-point"><label for="aph-add-navigation-point">${translate('addEntryPoint')}</label></div>
<div class="controls-container"><input type="checkbox" id="aph-inherit-navigation-point"><label for="aph-inherit-navigation-point">${translate('inheritEntryPoint')}</label></div>
<div class="controls-container"><input type="checkbox" id="aph-set-name"><label for="aph-set-name">${translate('copyHNToName')}</label></div>
<div class="controls-container"><input type="checkbox" id="aph-no-duplicates"><label for="aph-no-duplicates">${translate('noDuplicates')}</label></div>
</div>
`
new WazeWrap.Interface.Tab('APH📍', html)
var APHAddNavigationPoint = $('#aph-add-navigation-point')
var APHInheritNavigationPoint = $('#aph-inherit-navigation-point')
var APHSetName = $('#aph-set-name')
var APHNoDuplicates = $('#aph-no-duplicates')
APHAddNavigationPoint.change(() => {
settings.addNavigationPoint = APHAddNavigationPoint.prop('checked')
})
APHInheritNavigationPoint.change(() => {
settings.inheritNavigationPoint = APHInheritNavigationPoint.prop('checked')
})
APHSetName.change(() => {
settings.autoSetHNToName = APHSetName.prop('checked')
})
APHNoDuplicates.change(() => {
settings.noDuplicates = APHNoDuplicates.prop('checked')
})
}
function initSettings () {
var savedSettings = localStorage.getItem('aph-settings')
if (savedSettings) {
settings = JSON.parse(savedSettings)
}
setChecked('aph-add-navigation-point', settings.addNavigationPoint)
setChecked('aph-inherit-navigation-point', settings.inheritNavigationPoint)
setChecked('aph-set-name', settings.autoSetHNToName)
setChecked('aph-no-duplicates', settings.noDuplicates)
window.addEventListener('beforeunload', saveSettings)
}
function initLocale () {
locale = I18n.currentLocale()
}
function translate (keyword) {
let translation = translations[locale] || translations['en']
return translation[keyword] || translations['en'][keyword] || 'Unknown'
}
function saveSettings () {
if (localStorage) {
localStorage.setItem('aph-settings', JSON.stringify(settings))
}
}
function isValidSelection () {
if (!W.selectionManager.hasSelectedFeatures()) return false
if (W.selectionManager.getSelectedFeatures().length !== 1) return false
if (W.selectionManager.getSelectedFeatures()[0].model.type !== 'venue') return false
return true
}
function showButtons () {
if (!isValidSelection()) return
var buttons = `
<div id="aph-buttons" style="margin-top: 8px">
<div class="btn-toolbar">
<input type="button" id="aph-create-point" class="aph-btn btn btn-default" value="${translate('createPoint')}">
<input type="button" id="aph-create-residential" class="aph-btn btn btn-default" value="${translate('createResidential')}">
</div>
</div>
`
if (!$('#aph-buttons').length) {
$('#venue-edit-general .address-edit').append(buttons)
$('#aph-create-point').click(createPoint)
$('#aph-create-residential').click(createResidential)
}
const valid = validateSelectedPoiHN()
$('#aph-create-point').prop('disabled', !valid.validForPoint)
$('#aph-create-residential').prop('disabled', !valid.vadlidForResidential)
}
function validateSelectedPoiHN () {
let result = {
validForPoint: false,
vadlidForResidential: false
}
let country = W.model.getTopCountry().name
let validator = hnValidators[country] || hnValidators['default']
let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
result.vadlidForResidential = validator(selectedPoiHN)
result.validForPoint = /\d+/.test(selectedPoiHN)
return result
}
function createResidential () {
isValidSelection() && createPoint({ isResidential: true })
}
function createPoint ({ isResidential = false } = {}) {
if (!isValidSelection()) return
var LandmarkFeature = require('Waze/Feature/Vector/Landmark')
var AddLandmarkAction = require('Waze/Action/AddLandmark')
var UpdateObjectAction = require('Waze/Action/UpdateObject')
var UpdateFeatureAddressAction = require('Waze/Action/UpdateFeatureAddress')
var NewPoint = new LandmarkFeature()
var { lat, lon } = getPointCoordinates()
var address = getSelectedLandmarkAddress()
var lockRank = getPointLockRank()
var pointGeometry = new OpenLayers.Geometry.Point(lon, lat)
NewPoint.geometry = pointGeometry
NewPoint.attributes.categories.push('OTHER')
NewPoint.attributes.lockRank = lockRank
NewPoint.attributes.residential = isResidential
if (settings.addNavigationPoint) {
var entryPoint, parentEntryPoint = W.selectionManager.getSelectedFeatures()[0].model.attributes.entryExitPoints[0]
if (settings.inheritNavigationPoint && parentEntryPoint !== undefined) {
entryPoint = new NavigationPoint(parentEntryPoint.getPoint())
} else {
entryPoint = new NavigationPoint(pointGeometry.clone())
}
NewPoint.attributes.entryExitPoints.push(entryPoint)
}
if (!!address.attributes.houseNumber) {
NewPoint.attributes.name = address.attributes.houseNumber
NewPoint.attributes.houseNumber = address.attributes.houseNumber
}
var newAddressAttributes = {
streetName: address.getStreetName(),
emptyStreet: false,
cityName: address.getCityName(),
emptyCity: false,
stateID: address.getState().getID(),
countryID: address.getCountry().getID(),
}
if (settings.noDuplicates && hasDuplicate(NewPoint, newAddressAttributes)) {
console.log('This point already exists.')
return
}
W.selectionManager.unselectAll()
var addedLandmark = new AddLandmarkAction(NewPoint)
W.model.actionManager.add(addedLandmark)
W.model.actionManager.add(new UpdateFeatureAddressAction(NewPoint, newAddressAttributes))
if (!!address.attributes.houseNumber) {
W.model.actionManager.add(new UpdateObjectAction(NewPoint, {houseNumber: address.attributes.houseNumber}));
}
W.selectionManager.setSelectedModels([addedLandmark.venue])
}
function hasDuplicate (poi, addr) {
const venues = W.model.venues.objects
for (let key in venues) {
if (!venues.hasOwnProperty(key)) continue
const currentVenue = venues[key]
const currentAddress = currentVenue.getAddress()
let equalNames = true
if (!!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 getPointCoordinates () {
const selectedLandmarkGeometry = W.selectionManager.getSelectedFeatures()[0].geometry
var coords
if (/polygon/i.test(selectedLandmarkGeometry.id)) {
var polygonCenteroid = selectedLandmarkGeometry.components[0].getCentroid()
var geometryComponents = selectedLandmarkGeometry.components[0].components
var flatComponentsCoords = []
geometryComponents.forEach(c => flatComponentsCoords.push(c.x, c.y))
var interiorPoint = getInteriorPointOfArray(
flatComponentsCoords,
2, [polygonCenteroid.x, polygonCenteroid.y]
)
coords = {
lon: interiorPoint[0],
lat: interiorPoint[1]
}
} else {
coords = {
lon: selectedLandmarkGeometry.x,
lat: selectedLandmarkGeometry.y
}
}
coords = addRandomOffsetToCoords(coords)
return coords
}
function addRandomOffsetToCoords (coords) {
var { lat, lon } = coords
lat += Math.random() * 2 + 1
lon += Math.random() * 2 + 1
return { lat, lon }
}
function getSelectedLandmarkAddress () {
const selectedLandmark = W.selectionManager.getSelectedFeatures()[0]
const address = selectedLandmark.model.getAddress()
return address
}
function getPointLockRank () {
const selectedLandmark = W.selectionManager.getSelectedFeatures()[0]
const userRank = W.loginManager.user.rank
const parentFeatureLockRank = selectedLandmark.model.getLockRank()
if (userRank >= parentFeatureLockRank) {
return parentFeatureLockRank
} else if (userRank >= 1) {
return 1
} else {
return 0
}
}
function setChecked (checkboxId, checked) {
$('#' + checkboxId).prop('checked', checked)
}
function registerKeyboardShortcuts () {
const scriptName = 'AddressPointHelper'
WMEKSRegisterKeyboardShortcut(scriptName, 'Address Point Helper', 'APHCreatePoint', translate('createPoint'), createPoint, '-1')
WMEKSRegisterKeyboardShortcut(scriptName, 'Address Point Helper', 'APHCreateResidential', translate('createResidential'), createResidential, '-1')
WMEKSLoadKeyboardShortcuts(scriptName)
window.addEventListener('beforeunload', function () {
WMEKSSaveKeyboardShortcuts(scriptName)
}, false)
}
function registerEventListeners () {
let UpdateObjectAction = require('Waze/Action/UpdateObject')
W.model.actionManager.events.register('afteraction', null, action => {
// Задаем номер дома в название, если нужно. Пока не нашел более лаконичного способа определить что
// произошло именно изменение адреса. Можно тестить регуляркой поле _description, но будут проблемы с
// нюансами содержания этого поля на разных языках
if (settings.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 UpdateObjectAction(feature, { name: houseNumber }))
}
} catch (e) { /* Do nothing */ }
}
})
W.selectionManager.events.register('selectionchanged', null, showButtons)
W.model.actionManager.events.register('afteraction', null, showButtons)
setTimeout(wrapSelectionHandlers, 2000)
}
function wrapSelectionHandlers () {
// POI Helper trows error and breaks event handlers execution
// Wrap each handler in try/catch to fix this
let wrappedHandlers = W.selectionManager.events.listeners.selectionchanged.map(listener => {
return {
obj: listener.obj,
func: function () {
try {
listener.func.apply(this, arguments)
} catch (error) {
console.error(error)
}
}
}
})
W.selectionManager.events.listeners.selectionchanged = wrappedHandlers
}
/*
* 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
}
/* **************************************** */
var _createClass = function () {
function a (b, c) {for (var f, d = 0; d < c.length; d++) f = c[d], f.enumerable = f.enumerable || !1, f.configurable = !0, 'value' in f && (f.writable = !0), Object.defineProperty(b, f.key, f)}
return function (b, c, d) {return c && a(b.prototype, c), d && a(b, d), b}
}()
function _classCallCheck (a, b) {if (!(a instanceof b)) throw new TypeError('Cannot call a class as a function')}
var NavigationPoint = function () {
function a (b) {_classCallCheck(this, a), this._point = b.clone(), this._entry = !0, this._exit = !0, this._isPrimary = !0, this._name = ''}
return _createClass(a, [{
key: 'with',
value: function _with () {
var b = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}
return null == b.point && (b.point = this.toJSON().point), new this.constructor((this.toJSON().point, b.point))
}
}, { key: 'getPoint', value: function getPoint () {return this._point.clone()} }, {
key: 'getEntry',
value: function getEntry () {return this._entry}
}, { key: 'getExit', value: function getExit () {return this._exit} }, {
key: 'getName',
value: function getName () {return this._name}
}, { key: 'isPrimary', value: function isPrimary () {return this._isPrimary} }, {
key: 'toJSON',
value: function toJSON () {
return {
point: this._point,
entry: this._entry,
exit: this._exit,
primary: this._isPrimary,
name: this._name
}
}
}, { key: 'clone', value: function clone () {return this.with()} }]), a
}()