WME Segment City Highlighter

Highlighter to help out with cities on WME road segments

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址