// ==UserScript==
// @name WME Segment City Highlighter
// @namespace WazeDev
// @version 2024.09.07.002
// @description Highlighter to help out with cities on WME road segments
// @author MapOMatic
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @grant none
// ==/UserScript==
/* global W */
/* global _ */
/* global OpenLayers */
(function main() {
'use strict';
const SCRIPT_STORE = 'wme-sc-highlighter';
const SCRIPT_NAME = GM_info.script.name;
const NO_CITY_NAME = '<No city>';
const CSS = `
input.wmesch-city-input { height: 22px; }
.wmesch-clear-text { height: 22px; vertical-align: bottom; }
.wmesch-btn { height: 22px; float: right; }
#wmesch-container {border: #bbb 1px solid; border-radius: 4px; margin: 2px 10px; padding: 4px;}
#wmesch-container table { width: 100%; }
#wmesch-container td { vertical-align:top; }
#wmesch-container .header-label { float: right; }
#wmesch-container .header-cell { width: 90px; }
#wmesch-container .wmesch-city-input { width: 160px; }
.wmesch-preview { float: left; }
`;
const _lastValues = {};
let LAYER_Z_INDEX;
let _mapLayer;
let _$previewCheckbox;
let _$primaryCityText;
let _$altCityText;
function log(msg) {
console.log('WME SCH:', msg);
}
function _id(name) {
return `wmesch-${name}`;
}
function saveSettings() {
localStorage.setItem(SCRIPT_STORE, JSON.stringify({
primaryCity: _$primaryCityText.val(),
altCity: _$altCityText.val(),
preview: _$previewCheckbox.prop('checked')
}));
}
function loadSettings() {
const settings = $.parseJSON(localStorage.getItem(SCRIPT_STORE) || '{}');
_$primaryCityText.val(settings.primaryCity || '');
_$altCityText.val(settings.altCity || '');
_$previewCheckbox.prop('checked', settings.preview || false);
}
function updateCityLists() {
const cities = W.model.cities.getObjectArray()
.map(city => city.attributes.name)
.filter(name => name.length)
.sort()
.map(name => `<option value="${name}">`);
$(`#${_id('alt-city-datalist')}`).empty().append(cities);
cities.push(`<option value="${NO_CITY_NAME}">`);
$(`#${_id('primary-city-datalist')}`).empty().append(cities);
}
function getStreetInfo(streetID, isPrimary = false) {
const street = W.model.streets.getObjectById(streetID);
if (!street) {
return { ignore: true };
}
const city = W.model.cities.getObjectById(street.attributes.cityID);
if (!city) return { ignore: true };
const state = W.model.states.getObjectById(city.attributes.stateID);
const country = W.model.countries.getObjectById(city.getCountryID());
// If country is not found, it will be assumed the city is not a valid city and will be treated the
// same as a no - city segment. i.e. it wll be removed if primary or any alts have a city with the same street name.
return {
id: streetID,
streetName: street.attributes.name,
cityName: country ? W.model.cities.getObjectById(street.attributes.cityID).attributes.name : '',
stateID: state.attributes.id,
countryID: country ? country.attributes.id : -1,
isPrimary
};
}
function processSegments(segments) {
const roadTypesToIgnore = [18];
segments = segments.filter(s => roadTypesToIgnore.indexOf(s.attributes.roadType) === -1);
const newPrimaryCityName = $(`#${_id('primary-city')}`).val().trim();
const newAltCityName = $(`#${_id('alt-city')}`).val().trim();
const removeOtherAltCities = $(`#${_id('remove-other-alts')}`).prop('checked');
const result = { actions: [], affectedSegments: [], altIdsToRemove: [] };
segments.forEach(segment => {
const segmentAttr = segment.attributes;
let isSegmentEdited = false;
let isAllNoCity = !newPrimaryCityName && !newAltCityName;
if (segmentAttr.primaryStreetID) {
const primaryStreetInfo = getStreetInfo(segmentAttr.primaryStreetID, true);
const noPrimaryCity = newPrimaryCityName === NO_CITY_NAME;
if (newPrimaryCityName && ((!noPrimaryCity && primaryStreetInfo.cityName !== newPrimaryCityName)
|| (noPrimaryCity && !!primaryStreetInfo.cityName))) {
isSegmentEdited = true;
}
if (primaryStreetInfo.cityName || !primaryStreetInfo.streetName || primaryStreetInfo.ignore) {
isAllNoCity = false;
}
let streetInfos = [primaryStreetInfo];
if (noPrimaryCity) {
primaryStreetInfo.cityName = '';
} else if (newPrimaryCityName) {
primaryStreetInfo.cityName = newPrimaryCityName;
}
const altStreetInfos = segmentAttr.streetIDs.map(streetID => getStreetInfo(streetID));
streetInfos = streetInfos.concat(altStreetInfos);
if (!streetInfos.some(streetInfo => streetInfo.ignore)) {
let cityNames = _.uniq(streetInfos.map(streetInfo => streetInfo.cityName).filter(cityName => !!cityName));
if (cityNames.length) isAllNoCity = false;
if (newAltCityName && cityNames.indexOf(newAltCityName) === -1) cityNames.push(newAltCityName);
const streetNames = _.uniq(streetInfos.map(streetInfo => streetInfo.streetName).filter(streetName => !!streetName));
if (removeOtherAltCities) {
cityNames = cityNames.filter(cityName => cityName === newPrimaryCityName || cityName === newAltCityName);
}
cityNames.forEach(cityName => {
streetNames.forEach(streetName => {
if (!streetInfos.some(streetInfo => streetInfo.streetName === streetName && streetInfo.cityName === cityName)) {
isSegmentEdited = true;
streetInfos.push({ streetID: -999, streetName, cityName });
}
});
});
if (cityNames.length) {
const altIdsToRemove = altStreetInfos.filter(altStreetInfo => {
if (newPrimaryCityName && newPrimaryCityName === altStreetInfo.cityName
&& primaryStreetInfo.streetName === altStreetInfo.streetName) {
return true;
} if (!altStreetInfo.cityName) {
return true;
}
return false;
}).map(altStreetInfo => altStreetInfo.id);
if (altIdsToRemove.length) {
result.altIdsToRemove.push({
segment,
altIds: altIdsToRemove
});
isSegmentEdited = true;
}
}
}
}
if (isAllNoCity && !segment.isNew()) isSegmentEdited = true;
if (isSegmentEdited) result.affectedSegments.push(segment);
});
return result;
}
function highlightSegments() {
if (!_$previewCheckbox.prop('checked')) return;
_mapLayer.removeAllFeatures();
const result = processSegments(W.model.segments.getObjectArray(), true);
const features = W.map.segmentLayer.features.filter(f => result.affectedSegments.indexOf(f.attributes.wazeFeature._wmeObject) > -1).map(f => {
const geometry = f.geometry.clone();
const style = {
strokeColor: '#ff0',
strokeDashstyle: 'solid',
strokeWidth: 30
};
return new OpenLayers.Feature.Vector(geometry, null, style);
});
_mapLayer.addFeatures(features);
}
function onSegmentsAdded() {
highlightSegments();
}
function onCitiesAddedToModel() {
updateCityLists();
}
function onCityTextChange() {
const id = $(this).attr('id');
if (id) {
_lastValues[id] = $(this).val();
}
saveSettings();
highlightSegments();
}
function onClearTextClick() {
$(`#${_id($(this).attr('for'))}`).val(null).change();
}
function onPreviewChanged() {
saveSettings();
if (_$previewCheckbox.prop('checked')) {
highlightSegments();
W.model.segments.on('objectsadded', onSegmentsAdded);
W.model.segments.on('objectschanged', onSegmentsAdded);
} else {
_mapLayer.removeAllFeatures();
W.model.segments.off('objectsadded', onSegmentsAdded);
W.model.segments.off('objectschanged', onSegmentsAdded);
}
onSelectionChanged();
}
function onSelectionChanged() {
try {
const selected = W.selectionManager.getSelectedDataModelObjects()[0];
const isSegment = selected?.type === 'segment';
$(`#${_id('container')}`).css({ display: isSegment ? '' : 'none' });
} catch (ex) {
console.error(SCRIPT_NAME, ex);
}
}
function initGui() {
_$previewCheckbox = $('<input>', {
id: _id('preview'),
type: 'checkbox',
class: _id('preview')
});
_$primaryCityText = $('<input>', {
id: _id('primary-city'),
type: 'text',
class: _id('city-input'),
list: _id('primary-city-datalist'),
autocomplete: 'off' // helps prevent password manager from displaying a popup list
});
_$altCityText = $('<input>', {
id: _id('alt-city'),
type: 'text',
class: _id('city-input'),
list: _id('alt-city-datalist'),
autocomplete: 'off' // helps prevent password manager from displaying a popup list
});
// TODO: 2022-11-22 - This is temporary to determine which parent element to add the div to, depending on beta or production WME.
// Remove once new side panel is pushed to production.
const $parent = $('#edit-panel .contents');
$parent.prepend(
$('<div>', { id: _id('container') }).append(
$('<table>').append(
$('<tr>').append(
$('<td>', { class: 'header-cell' }).append($('<label>', { class: 'header-label' }).text('Primary city')),
$('<td>').append(
_$primaryCityText,
$('<button>', { class: _id('clear-text'), for: 'primary-city' }).text('x')
)
),
$('<tr>').append(
$('<td>', { class: 'header-cell' }).append($('<label>', { class: 'header-label' }).text('Alt city')),
$('<td>').append(
_$altCityText,
$('<button>', { class: _id('clear-text'), for: 'alt-city' }).text('x')
)
),
$('<tr>').append($('<td>', { colspan: '2', class: _id('run-button-container') }).append(
$('<div>').append(
$('<div>', { class: `controls-container ${_id('preview')}` }).append(
_$previewCheckbox.change(onPreviewChanged),
$('<label>', { for: _id('preview') }).text('Preview')
)
)
))
),
$('<datalist>', { id: _id('primary-city-datalist') }),
$('<datalist>', { id: _id('alt-city-datalist') })
)
);
$(`.${_id('clear-text')}`).click(onClearTextClick);
$(`.${_id('city-input')}`).each2((idx, obj) => {
const [{ id }] = obj;
const lastVal = _lastValues[id];
if (lastVal) obj.val(lastVal);
}).change(onCityTextChange);
updateCityLists();
loadSettings();
onPreviewChanged();
}
function initLayer() {
_mapLayer = new OpenLayers.Layer.Vector('WME Segment City Highlighter', { uniqueName: '__wmeSegmentCityHighlighter' });
W.map.addLayer(_mapLayer);
// W.map.setLayerIndex(_mapLayer, W.map.getLayerIndex(W.map.roadLayers[0])-2);
// HACK to get around conflict with URO+. If URO+ is fixed, this can be replaced with the setLayerIndex line above.
LAYER_Z_INDEX = W.map.roadLayer.getZIndex() - 2;
_mapLayer.setZIndex(LAYER_Z_INDEX);
const checkLayerZIndex = () => { if (_mapLayer.getZIndex() !== LAYER_Z_INDEX) _mapLayer.setZIndex(LAYER_Z_INDEX); };
setInterval(() => { checkLayerZIndex(); }, 100);
// END HACK
_mapLayer.setOpacity(0.6);
_mapLayer.setVisibility(true);
}
function init() {
$(`<style type="text/css">${CSS}</style>`).appendTo('head');
W.model.cities.on('objectsadded', onCitiesAddedToModel);
W.selectionManager.events.register('selectionchanged', null, onSelectionChanged);
initLayer();
initGui();
}
function bootstrap(tries = 1) {
if (W && W.loginManager && W.loginManager.user && $('#sidebar').length) {
init();
} else if (tries > 200) {
log('Bootstrap has failed too many times. Exiting script.');
} else {
if (tries % 20 === 0) log('Bootstrap failed. Trying again...');
setTimeout(() => bootstrap(++tries), 250);
}
}
bootstrap();
})();