WME Closest Segments

Tìm tên đường gần nhất của venue (Place) đang được chọn

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WME Closest Segments
// @namespace    https://greasyfork.org/
// @version      1.0.0
// @description  Tìm tên đường gần nhất của venue (Place) đang được chọn
// @author       Minh Tan
// @match        https://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @exclude      https://www.waze.com/*user/editor*
// @grant        GM_xmlhttpRequest
// @grant        GM.setClipboard
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require      https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// @require      https://update.greasyfork.org/scripts/450221/1691071/WME-Base.js
// @require      https://update.greasyfork.org/scripts/450320/1688694/WME-UI.js
// @require      https://cdn.jsdelivr.net/npm/@turf/[email protected]/turf.min.js
// ==/UserScript==
/* global require */
/* global $, jQuery */
/* global I18n */
/* global WMEBase, WMEUI, WMEUIHelper, WMEUIHelperFieldset */
/* global Container, Settings, SimpleCache, Tools  */
/* global Node$1, Segment, Venue, VenueAddress, WmeSDK */
/* global W, WazeWrap, $, XLSX */
/* global turf */

(function() {
    'use strict';
    const NAME = 'ClosestSeg'
    const LOCALE = {
        240: {
            country: 'vn',
            language: 'en',
            locale: 'vi_VN'
        }
    }
    const SETTINGS = {
        options: {
            modal: true,
            transparent: false,
            entryPoint: true,
            lock: true,
        },
        ranges: {
            radius: 200,
            collapse: 3,
        },
        providers: {
            magic: true
        }
    }

    // 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
    }
    const TRANSLATION = {
        'en': {
            title: 'Closest Segments',
            notFound: 'Not found',
            options: {
                title: 'Options',
                modal: 'Use modal window',
                transparent: 'Transparent modal window',
                entryPoint: 'Create Entry Point if not exists',
                lock: 'Lock POI to 2 level',
            },
            ranges: {
                title: 'Additional',
                radius: 'Radius for search',
                collapse: 'Collapse the lists longer than',
            },
            providers: {
                title: 'Providers',
                magic: 'Closest Segments',
            },
            questions: {
                changeName: 'Are you sure to change the name?',
                changeCity: 'Are you sure to change the city?',
                changeStreet: 'Are you sure to change the street name?',
                changeNumber: 'Are you sure to change the house number?',
                notFoundCity: 'City not found in the current location, are you sure to create a new one?',
                notFoundStreet: 'Street not found in the current location, are you sure to create a new one?'
            }
        }
    }
    WMEUI.addTranslation(NAME, TRANSLATION)
    const STYLE =
          '.form-group.closestseg .header h5 { padding: 16px 16px 0; font-size: 16px }' +
          '.form-group.closestseg .body { overflow-x: auto; max-height: 420px; padding: 4px 0; }' +
          '#venue-edit-general .closestseg fieldset { border: 0; padding: 0; margin: 0; }' +
          '#venue-edit-general .closestseg legend { width: 100%; text-align: left; }' +
          '#venue-edit-general .closestseg fieldset legend,        .wme-ui-panel.closestseg fieldset legend { cursor:pointer; font-size: 12px; font-weight: bold; margin: 0; padding: 0 8px; background-color: #f6f7f7; border: 1px solid #e5e5e5 }' +
          '#venue-edit-general .closestseg fieldset legend::after, .wme-ui-panel.closestseg fieldset legend::after { display: inline-block; text-rendering: auto; content: "↑"; float: right; font-size: 10px; line-height: inherit; position: relative; right: 3px; } ' +
          '#venue-edit-general .closestseg fieldset legend span,   .wme-ui-panel.closestseg fieldset legend span { font-weight: bold; background-color: #fff; border-radius: 5px; color: #ed503b; display: inline-block; font-size: 12px; line-height: 14px; max-width: 30px; padding: 1px 5px; text-align: center; } ' +
          '#venue-edit-general .closestseg fieldset ul,            .wme-ui-panel.closestseg fieldset ul { border: 1px solid #ddd; } ' +
          '#venue-edit-general .closestseg fieldset.collapsed ul,  .wme-ui-panel.closestseg fieldset.collapsed ul { display: none } ' +
          '#venue-edit-general .closestseg fieldset.collapsed legend::after, .wme-ui-panel.closestseg fieldset.collapsed legend::after { content: "↓" }' +
          '#venue-edit-general .closestseg ul, .wme-ui-panel.closestseg ul { padding: 8px; margin: 0 }' +
          '#venue-edit-general .closestseg li, .wme-ui-panel.closestseg li { padding: 0; margin: 0; list-style: none; margin-bottom: 2px }' +
          '#venue-edit-general .closestseg li a, .wme-ui-panel.closestseg li a { display: block; padding: 2px 4px; text-decoration: none; border: 1px solid #e4e4e4; }' +
          '#venue-edit-general .closestseg li a:hover, .wme-ui-panel.closestseg li a:hover { background: rgba(255, 255, 200, 1) }' +
          '#venue-edit-general .closestseg li a.nonumber, .wme-ui-panel.closestseg li a.nonumber { background: rgba(250, 250, 200, 0.5) }' +
          '#venue-edit-general .closestseg li a.nonumber:hover, .wme-ui-panel.closestseg li a.nonumber:hover { background: rgba(250, 250, 200, 1) }' +
          '#venue-edit-general .closestseg li a.noaddress, .wme-ui-panel.closestseg li a.noaddress { background: rgba(250, 200, 100, 0.5) }' +
          '#venue-edit-general .closestseg li a.noaddress:hover, .wme-ui-panel.closestseg li a.noaddress:hover { background: rgba(250, 200, 100, 1) }' +
          '.form-group.closestseg legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
          '.form-group.closestseg fieldset { border: 1px solid #ddd; padding: 8px; }' +
          '.form-group.closestseg div.controls { padding: 8px; }' +
          '.form-group.closestseg div.controls:empty, #panel-container .archive-panel .body:empty { min-height: 20px; }' +
          '.form-group.closestseg div.controls:empty::after, #panel-container .archive-panel .body:empty::after { color: #ccc; padding: 0 8px; content: "' + I18n.t(NAME).notFound + '" }' +
          '.form-group.closestseg div.controls label { white-space: normal; font-weight: normal; margin-top: 5px; line-height: 18px; font-size: 13px; }' +
          '.form-group.closestseg div.controls input[type="text"] { float:right; }' +
          '.form-group.closestseg div.controls input[type="number"] { float:right; width: 60px; text-align:right; }' +
          '.distance-over-200 { background-color: #f08a24; }' +
          '.distance-over-1000 { background-color: #ed503b; }' +
          '.external-operational a.url { border: 4px solid #009900; border-radius: 50% }' +
          '.external-closed-temporarily a.url { border: 4px solid #ff7300; border-radius: 50%  }' +
          '.external-closed-permanently a.url { border: 4px solid #ff0000; border-radius: 50%  }' +
          'p.closestseg-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }' +
          '#sidebar p.closestseg-blue { background-color:#0057B8;color:white;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }' +
          '#sidebar p.closestseg-yellow { background-color:#FFDD00;color:black;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }'
    WMEUI.addStyle(STYLE)
    const layerConfig = {
        defaultRule: {
            styleContext: {
                label: (context) => {
                    const style = context?.feature?.properties?.style;
                    if (!style)
                        return style;
                    return style?.label;
                },
            },
            styleRules: [
                {
                    predicate: (properties) => properties.styleName === "styleNode",
                    style: {
                        pointRadius: 8,
                        fillOpacity: 0.5,
                        fillColor: '#fff',
                        strokeColor: '#fff',
                        strokeWidth: 2,
                        strokeLinecap: 'round',
                        graphicZIndex: 9999,
                    },
                },
                {
                    predicate: (properties) => properties.styleName === "styleLine",
                    style: {
                        strokeWidth: 4,
                        strokeColor: '#fff',
                        strokeLinecap: 'round',
                        strokeDashstyle: 'dash',
                        label: "${label}",
                        labelOutlineColor: '#000',
                        labelOutlineWidth: 3,
                        labelAlign: 'cm',
                        fontColor: '#fff',
                        fontSize: '24px',
                        fontFamily: 'Courier New, monospace',
                        fontWeight: 'bold',
                        labelYOffset: 24,
                        graphicZIndex: 9999,
                    }
                }
            ],
        },
    };
    let E50Instance, E50Cache
    class ClosestSeg extends WMEBase {
        constructor(name, settings) {
            super(name, settings)
            this.initHelper()
            this.initTab()
            this.initLayer()
        }
        initHelper() {
            this.helper = new WMEUIHelper(this.name)
            this.modal = this.helper.createModal(I18n.t(this.name).title)
            this.panel = this.helper.createPanel(I18n.t(this.name).title)
        }
        initTab() {
            let tab = this.helper.createTab(
                I18n.t(this.name).title,
                {
                    sidebar: this.wmeSDK.Sidebar,
                    image: GM_info.script.icon
                }
            )
            // Setup options
            /** @type {WMEUIHelperFieldset} */
            let fsOptions = this.helper.createFieldset(I18n.t(this.name).options.title)
            let options = this.settings.get('options')
            for (let item in options) {
                if (options.hasOwnProperty(item)) {
                    fsOptions.addCheckbox(
                        item,
                        I18n.t(this.name).options[item],
                        (event) => this.settings.set(['options', item], event.target.checked),
                        this.settings.get('options', item)
                    )
                }
            }
            tab.addElement(fsOptions)
            // Setup ranges
            /** @type {WMEUIHelperFieldset} */
            let fsRanges = this.helper.createFieldset(I18n.t(this.name).ranges.title)
            let ranges = this.settings.get('ranges')
            for (let item in ranges) {
                if (ranges.hasOwnProperty(item)) {
                    fsRanges.addNumber(
                        'settings-ranges-' + item,
                        I18n.t(NAME).ranges[item],
                        event => this.settings.set(['ranges', item], event.target.value),
                        this.settings.get('ranges', item),
                        (item === 'radius') ? 100 : 0,
                        (item === 'radius') ? 1000 : 10,
                        (item === 'radius') ? 50 : 1
                    )
                }
            }
            tab.addElement(fsRanges)
            tab.addText(
                'info',
                '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
            )
            tab.inject()
        }
        initLayer() {
            this.wmeSDK.Map.addLayer({
                layerName: this.name,
                styleRules: layerConfig.defaultRule.styleRules,
                styleContext: layerConfig.defaultRule.styleContext
            });
            // this.wmeSDK.LayerSwitcher.addLayerCheckbox({ name: this.name });
            this.wmeSDK.Map.setLayerZIndex({ layerName: this.name, zIndex: 9999 });
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
        }
        /**
         * Create the vector from the center of the selected POI to point by lon and lat
         * @param {Number} lon
         * @param {Number} lat
         */
        createVector(lon, lat) {
            let poi = this.getSelectedPOI()
            if (!poi) {
                return
            }
            const from = turf.centroid(poi.geometry)
            const to = turf.point([lon, lat], { styleName: "styleNode" }, { id: `node_${lon}_${lat}` });
            this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: to });
            const lineCoordinates = [
                from.geometry.coordinates,
                to.geometry.coordinates,
            ];
            const distance = Math.round(turf.distance(to, from) * 1000)
            const label = (distance > 2000)
            ? (distance / 1000).toFixed(1) + 'km'
            : distance + 'm'
            // https://www.waze.com/editor/sdk/interfaces/index.SDK.FeatureStyle.html
            const line = turf.lineString(lineCoordinates, {
                styleName: "styleLine",
                style: {
                    label: label,
                },
            }, { id: `line_${lon}_${lat}` });
            this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: line });
        }
        /**
         * Remove all vectors from the layer
         */
        removeVectors() {
            this.wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: this.name });
        }
        /**
         * Show the Layer
         */
        showLayer() {
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: true });
        }
        /**
         * Hide the Layer
         */
        hideLayer() {
            this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
        }
        /**
         * Handler for `none.wme` event
         * @param {jQuery.Event} event
         * @return {Null}
         */
        onNone(event) {
            if (this.settings.get('options', 'modal')) {
                this.modal.html().remove()
            }
        }
        /**
         * Handler for `venue.wme` event
         *  - create and fill the modal panel
         *
         * @param {jQuery.Event} event
         * @param {HTMLElement} element
         * @param {Venue} model
         * @return {null|void}
         */
        onVenue(event, element, model) {
            let container, parent
            if (this.settings.get('options', 'modal')) {
                parent = this.modal.html()
                container = parent.querySelector('.wme-ui-body')
            } else {
                parent = this.panel.html()
                container = parent.querySelector('.controls')
            }
            // Clear container
            try {
                if (container)
                    while (container.hasChildNodes()) {
                        container.removeChild(container.lastChild)
                    }
            } catch (e) {
                console.error(e)
            }
            if (!model) {
                return
            }
            let feature = turf.centroid(model.geometry)
            let [lon, lat] = feature.geometry.coordinates;
            let providers = []
            let country = this.wmeSDK.DataModel.Countries.getTopCountry()?.id || 240
            let settings = LOCALE[country]
            this.group(
                '📍' + lon + ' ' + lat
            )
            let radius = this.settings.get('ranges', 'radius')
            if (this.settings.get('providers', 'magic')) {
                let Magic = new MagicProvider(container, settings)
                let providerPromise = Magic
                .search(lon, lat, radius)
                .then(() => Magic.render())
                .catch(() => this.log(':('))
                providers.push(providerPromise)
            }
            Promise
                .all(providers)
                .then(() => this.groupEnd())
            if (this.settings.get('options', 'modal')) {
                if (this.settings.get('options', 'transparent')) {
                    parent.style.opacity = '0.6'
                    parent.onmouseover = () => (parent.style.opacity = '1')
                    parent.onmouseout = () => (parent.style.opacity = '0.6')
                }
                this.modal.container().append(parent)
            } else {
                element.prepend(parent)
            }
        }
        /**
         * Get Selected Venue if it not the NATURAL_FEATURES
         * @return {null|Object}
         */
        getSelectedPOI() {
            let venue = this.getSelectedVenues().shift()
            if (!venue) {
                return null
            }
            let except = ['NATURAL_FEATURES']
            if (except.indexOf(venue.categories[0]) === -1) {
                return venue
            }
            return null
        }
        /**
        * Apply data to the current selected place
        * @param {Object} data
        */
        applyData(data) {
            let venue = this.getSelectedPOI()
            if (!this.wmeSDK.DataModel.Venues.hasPermissions({ venueId: venue.id })) {
                this.log('You don\'t have permissions to edit this venue')
                return
            }
            let address = this.wmeSDK.DataModel.Venues.getAddress({ venueId: venue.id })
            let lat = parseFloat(data.lat)
            let lon = parseFloat(data.lon)
            if (isNaN(lat) || isNaN(lon)) {
                this.log('Invalid coordinates')
                return
            }
            this.group('Apply data to selected Venue ↓')
            let name = data.name ? data.name.trim() : ''
            let cityId = isNaN(parseInt(data.cityId)) ? null : parseInt(data.cityId)
            let cityName = data.cityName ? data.cityName.trim() : ''
            let streetId = isNaN(parseInt(data.streetId)) ? null : parseInt(data.streetId)
            let streetName = data.streetName ? data.streetName.trim() : ''
            let number = data.number ? data.number.trim() : ''
            // Apply new Name
            let newName
            // If exists, ask the user to replace it or not
            // If not exists - use name or house number as name
            if (venue.name) {
                this.log('The Venue has a Name «' + venue.name + '»')
                if (name && name !== venue.name) {
                    this.log('Replace a Venue Name with a new one?')
                    if (window.confirm(I18n.t(NAME).questions.changeName + '\n«' + venue.name + '» ⟶ «' + name + '»?')) {
                        newName = name
                        this.log(' — Yes, a new Venue Name is «' + newName + '»')
                    } else {
                        newName = venue.name
                        this.log(' — No, use a old Venue Name «' + newName + '»')
                    }
                } else if (number && number !== venue.name) {
                    this.log('Replace the Venue Name with a number?')
                    if (window.confirm(I18n.t(NAME).questions.changeName + '\n«' + venue.name + '» ⟶ «' + number + '»?')) {
                        newName = number
                        this.log(' — Yes, a new Venue Name is «' + newName + '»')
                    } else {
                        newName = venue.name
                        this.log(' — No, use a old Venue Name «' + newName + '»')
                    }
                }
            } else if (name) {
                newName = name
                this.log('Use a new Venue Name «' + newName + '»')
            } else if (number) {
                newName = number
                this.log('Use a new Venue Name «' + newName + '»')
                // Update alias for korpus
                if ((new RegExp('[0-9]+[а-яі]?к[0-9]+', 'i')).test(number)) {
                    let alias = number.replace('к', ' корпус ')
                    let aliases = venue.aliases?.slice() || []
                    if (aliases.indexOf(alias) === -1) {
                        aliases.push(alias)
                        this.log('Apply a new Venue Alias «' + alias + '»')
                        this.wmeSDK.DataModel.Venues.updateVenue({
                            venueId: venue.id,
                            aliases: aliases
                        })
                    }
                }
            }
            // Set only really new name
            if (newName && newName !== venue.name) {
                this.log('Apply a new Venue Name «' + newName + '»')
                this.wmeSDK.DataModel.Venues.updateVenue({
                    venueId: venue.id,
                    name: newName
                })
            }
            // Apply a City name
            if (!cityId && cityName) {
                this.log('We don\'t find a City with name «' + cityName + '», create a new one?')
                // Ask to create a new City
                if (window.confirm(I18n.t(NAME).questions.notFoundCity + '\n«' + cityName + '»?')) {
                    cityId = this.getCity(cityName).id
                    this.log(' — Yes, create new City «' + cityName + '»')
                } else {
                    cityId = this.getCity().id
                    this.log(' — No, use the empty City with ID «' + cityId + '»')
                }
            } else if (!cityId && !cityName) {
                cityId = this.getCity().id
                this.log('We don\'t find a City and use the empty City with ID «' + cityId + '»')
            }
            let city = this.getCityById(cityId)
            let newStreetId
            // Apply a new Street
            if (streetId && address.street
                && streetId !== address.street.id
                && '' !== address.street.name) {
                this.log('Replace the Street with a new one?')
                if (window.confirm(I18n.t(NAME).questions.changeStreet + '\n«' + address.street.name + '» ⟶ «' + streetName + '»?')) {
                    newStreetId = streetId
                    this.log(' — Yes, use a new Street Name «' + streetName + '»')
                } else {
                    this.log(' — No, use a old Street Name «' + address.street.name + '»')
                }
            } else if (streetId) {
                newStreetId = streetId
                this.log('Use a new Street with ID «' + newStreetId + '»')
            } else if (!streetId) {
                let street
                if (streetName) {
                    this.log('We don\'t find the street «' + streetName + '»')
                    this.log('Create a new Street?')
                    if (window.confirm(I18n.t(NAME).questions.notFoundStreet + '\n«' + streetName + '»?')) {
                        street = this.getStreet(city.id, streetName)
                        this.log(' — Yes, create a new Street «' + streetName + '»')
                    } else if ('' !== address.street?.name) {
                        street = this.wmeSDK.DataModel.Streets.getById({ streetId: address.street.id })
                        this.log(' — No, use the current Street «' + street.name + '»')
                    } else {
                        street = this.getStreet(city.id, '')
                        this.log(' — No, use the empty Street with ID «' + street.id + '»')
                    }
                } else {
                    this.log('We don\'t find the street')
                    street = this.getStreet(city.id, '')
                    this.log('Use the empty Street with ID «' + street.id + '»')
                }
                if (street.id !== address.street?.id && '' !== address.street?.name) {
                    this.log('Replace the Street with new one?')
                    if (window.confirm(I18n.t(NAME).questions.changeStreet + '\n«' + address.street.name + '» ⟶ «' + streetName + '»?')) {
                        newStreetId = street.id
                        this.log(' — Yes, use a new Street Name «' + streetName + '»')
                    } else {
                        this.log(' — No, use the current Street Name «' + address.street.name + '»')
                    }
                } else {
                    newStreetId = street.id
                }
            }
            if (newStreetId && newStreetId !== address.street?.id) {
                this.log('Apply a new Street ID «' + newStreetId + '»')
                this.wmeSDK.DataModel.Venues.updateAddress({
                    venueId: venue.id,
                    streetId: newStreetId
                })
            }
            let newHouseNumber
            // Apply a House Number
            if (number) {
                if (address.houseNumber) {
                    this.log('Replace the House Number with a new one?')
                    if (address.houseNumber !== number &&
                        window.confirm(I18n.t(NAME).questions.changeNumber + '\n«' + address.houseNumber + '» ⟶ «' + number + '»?')) {
                        newHouseNumber = number
                        this.log(' — Yes, use a new House Number «' + number + '»')
                    } else {
                        this.log(' — No, use the current House Number «' + address.houseNumber + '»')
                    }
                } else {
                    newHouseNumber = number
                    this.log('Use a new House Number «' + number + '»')
                }
            }
            if (newHouseNumber) {
                this.log('Apply a new House Number «' + newHouseNumber + '»')
                this.wmeSDK.DataModel.Venues.updateAddress({
                    venueId: venue.id,
                    houseNumber: newHouseNumber
                })
            }
            // Lock to level 2
            if (this.settings.get('options', 'lock')
                && venue.lockRank < 1
                && this.wmeSDK.State.getUserInfo().rank > 0) {
                this.log('Apply a new Lock Rank «' + (1 + 1) + '»')
                this.wmeSDK.DataModel.Venues.updateVenue({
                    venueId: venue.id,
                    lockRank: 1
                })
            }
            // If no an entry point, we would create it
            if (this.settings.get('options', 'entryPoint')
                && venue.navigationPoints?.length === 0) {
                let point = turf.point([lon, lat])
                if (venue.geometry.type === 'Point') {
                    this.log('Use the coordinates for new Navigation Point for Point')
                } else if (turf.pointsWithinPolygon(point, venue.geometry).features?.length > 0) {
                    this.log('Use the coordinates for new Navigation Point inside Polygon')
                } else {
                    // point is outside the venue geometry
                    this.log('Use the intersection of Polygon and vector to coordinates as new Navigation Point')
                    let centroid = turf.centroid(venue.geometry);
                    let line = turf.lineString([
                        centroid.geometry.coordinates,
                        point.geometry.coordinates,
                    ]);
                    let featureCollection = turf.lineIntersect(venue.geometry, line);
                    point = featureCollection.features?.pop()
                }
                // create a navigation point
                let navigationPoint = {
                    isEntry: true,
                    isExit: false,
                    isPrimary: true,
                    name: "",
                    point: point.geometry
                }
                this.wmeSDK.DataModel.Venues.replaceNavigationPoints({
                    venueId: venue.id,
                    navigationPoints: [navigationPoint]
                })
            }
            this.groupEnd()
        }
        getCityById(cityID) {
            if (!cityID || isNaN(parseInt(cityID))) {
                return null
            }
            return this.wmeSDK.DataModel.Cities.getById({
                cityId: cityID
            })
        }
        getCity(cityName = '') {
            return this.wmeSDK.DataModel.Cities.getCity({
                countryId: this.wmeSDK.DataModel.Countries.getTopCountry().id,
                cityName: cityName
            })
            || this.wmeSDK.DataModel.Cities.addCity({
                countryId: this.wmeSDK.DataModel.Countries.getTopCountry().id,
                cityName: cityName
            })
        }
        getStreet(cityId, streetName = '') {
            return this.wmeSDK.DataModel.Streets.getStreet({
                cityId: cityId,
                streetName: streetName,
            })
            || this.wmeSDK.DataModel.Streets.addStreet({
                cityId: cityId,
                streetName: streetName
            })
        }
    }
    /**
* Basic Provider class
*/
    class Provider {
        constructor(uid, container, settings) {
            this.uid = uid.trim().toLowerCase().replace(/\s/g, '-')
            this.name = uid
            this.response = []
            this.settings = settings
            // prepare DOM
            this.panel = this._panel()
            this.container = container
            this.container.append(this.panel)
        }
        /**
        * @param {String} url
        * @param {Object} data
        * @returns {Promise<unknown>}
        */
        async makeRequest(url, data) {
            let query = new URLSearchParams(data).toString()
            if (query.length) {
                url = url + '?' + query
            }
            return new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: 'GET',
                    responseType: 'json',
                    url: url,
                    onload: response => response && response.response && resolve(response.response) || reject(response),
                    onabort: response => reject(response),
                    onerror: response => reject(response),
                    ontimeout: response => reject(response),
                })
            })
        }
        /**
        * @param  {Number} lon
        * @param  {Number} lat
        * @param  {Number} radius
        * @return {Promise<array>}
        */
        async request(lon, lat, radius) {
            throw new Error('Abstract method')
        }
        /**
        * @param  {Number} lon
        * @param  {Number} lat
        * @param  {Number} radius
        * @return {Promise<void>}
        */
        async search(lon, lat, radius = 1000) {
            let key = this.uid + ':' + lon + ',' + lat
            if (E50Cache.has(key)) {
                this.response = E50Cache.get(key)
            } else {
                this.response = await this.request(lon, lat, radius).catch(e => console.error(this.uid, 'search return error', e))
                E50Cache.set(key, this.response)
            }
            return new Promise((resolve, reject) => {
                if (this.response) {
                    resolve()
                } else {
                    reject()
                }
            })
        }
        /**
        * @param  {Array} res
        * @return {Array}
        */
        collection(res) {
            let result = []
            for (let i = 0; i < res.length; i++) {
                result.push(this.item(res[i]))
            }
            result = result.filter(x => x)
            return result
        }
        /**
        * Should return {Object}
        * @param  {Object} res
        * @return {Object}
        */
        item(res) {
            throw new Error('Abstract method')
        }
        /**
        * @param  {Number} lon
        * @param  {Number} lat
        * @param  {String} city
        * @param  {String} street
        * @param  {String} number
        * @param  {String} name
        * @param  {String} reference
        * @return {{number: *, cityId: Number, cityName: *, streetId: Number, streetName: *, name: *, raw: *, lon: *, title: *, lat: *}}
        */
        element(lon, lat, city, street, number, name = '', reference = '') {
            // Raw data from provider
            let raw = [city, street, number, name].filter(x => !!x).join(', ')
            let [cityId, cityName] = detectCity(city)
            let [streetId, streetName] = detectStreet(cityId, street)
            if (!cityId && streetId) {
                let streetModel = E50Instance.wmeSDK.DataModel.Streets.getById({ streetId: streetId })
                let cityModel = E50Instance.wmeSDK.DataModel.Cities.getById({ cityId: streetModel.cityId })
                cityId = cityModel.id
                cityName = cityModel.name
            }
            let title = [street, number, name].filter(x => !!x).join(', ')
            return {
                lat: lat,
                lon: lon,
                cityId: cityId,
                cityName: cityName,
                streetId: streetId,
                streetName: streetName,
                number: number,
                name: name,
                title: title,
                raw: raw,
                reference: reference
            }
        }
        /**
        * Render result to target element
        */
        render() {
            if (this.response.length === 0) {
                // remove empty panel
                this.panel.remove()
                return
            }
            this.panel.append(this._fieldset())
        }
        /**
        * Create div for all items
        * @return {HTMLDivElement}
        * @private
        */
        _panel() {
            let div = document.createElement('div')
            div.id = NAME.toLowerCase() + '-' + this.name
            div.className = NAME.toLowerCase()
            return div
        }
        /**
        * Build fieldset with the list of the response items
        * @return {HTMLFieldSetElement}
        * @protected
        */
        _fieldset() {
            let fieldset = document.createElement('fieldset')
            let list = document.createElement('ul')
            let collapse = parseInt(E50Instance.settings.get('ranges', 'collapse'))
            if (collapse && this.response.length > collapse) {
                fieldset.className = 'collapsed'
            } else {
                fieldset.className = ''
            }
            for (let i = 0; i < this.response.length; i++) {
                let item = document.createElement('li')
                item.append(this._link(this.response[i]))
                list.append(item)
            }
            let legend = document.createElement('legend')
            legend.innerHTML = this.name + ' <span>' + this.response.length + '</span>'
            legend.onclick = function () {
                this.parentElement.classList.toggle("collapsed")
                return false
            }
            fieldset.append(legend, list)
            return fieldset
        }
        /**
        * Build link by {Object}
        * @param  {Object} item
        * @return {HTMLAnchorElement}
        * @protected
        */
        _link(item) {
            let a = document.createElement('a')
            a.href = '#'
            a.dataset.lat = item.lat
            a.dataset.lon = item.lon
            a.dataset.cityId = item.cityId || ''
            a.dataset.cityName = item.cityName || ''
            a.dataset.streetId = item.streetId || ''
            a.dataset.streetName = item.streetName || ''
            a.dataset.number = item.number
            a.dataset.name = item.name
            a.dataset.reference = item.reference || ''
            a.innerText = item.title || item.raw
            a.title = item.raw
            a.className = NAME + '-link'
            if (!item.cityId || !item.streetId) {
                a.className += ' noaddress'
            }
            if (!item.number) {
                a.className += ' nonumber'
            }
            return a
        }
    }
    /**
* Based on the closest segment and city
*/
    class MagicProvider extends Provider {
        constructor(container, settings) {
            super(I18n.t(NAME).providers.magic, container, settings)
        }
        async request(lon, lat, radius) {
            let segments = E50Instance.getAllSegments(
                [TYPES.boardwalk, TYPES.stairway, TYPES.railroad, TYPES.runway, TYPES.parking]
            )
            let streets = {}
            console.groupCollapsed(this.uid)
            for (let key in segments) {
                let segment = segments[key]
                let address = E50Instance.wmeSDK.DataModel.Segments.getAddress({ segmentId: segment.id })
                if (address.street.name === '') {
                    continue
                }
                let distance = turf.pointToLineDistance(
                    turf.point([lon, lat]),
                    segment.geometry,
                    {
                        units: 'meters'
                    }
                )
                if (!streets[address.street.id]
                    || distance < streets[address.street.id].distance) {
                    let nearestPointOnLine = turf.nearestPointOnLine(
                        segment.geometry,
                        turf.point([lon, lat])
                    )
                    streets[address.street.id] = {
                        lon: nearestPointOnLine.geometry.coordinates[0],
                        lat: nearestPointOnLine.geometry.coordinates[1],
                        streetId: address.street.id,
                        streetName: address.street.name,
                        cityId: address.city.id,
                        cityName: address.city.name,
                        number: '',
                        name: '',
                        title: address.street.name,
                        raw: address.street.name + ', ' + address.city.name,
                        distance: distance,
                    }
                }
            }
            let result = []
            for (let key in streets) {
                if (streets.hasOwnProperty(key) && streets[key].distance <= radius) {
                    result.push(streets[key])
                }
            }
            result.sort((a, b) => {
                if (a.distance < b.distance) {
                    return -1;
                }
                if (a.distance > b.distance) {
                    return 1;
                }
                return 0;
            })
            return result
        }
    }
    $(document)
        .on('bootstrap.wme', ready)
        .on('click', '.' + NAME + '-link', applyData)
        .on('mouseenter', '.' + NAME + '-link', showLayer)
        .on('mouseleave', '.' + NAME + '-link', hideLayer)
        .on('mouseenter', '.' + NAME + '-external', showLayer)
        .on('mouseleave', '.' + NAME + '-external', hideLayer)
        .on('none.wme', hideLayer)
    /**
    * Initializes the `E50Instance` and `E50Cache` objects with predefined configurations.
    *
    * @return {void} This function does not return a value.
    */
    function ready() {
        E50Instance = new ClosestSeg(NAME, SETTINGS)
        E50Cache = new SimpleCache()
    }
    /**
* Apply data to the current selected POI
* @param event
*/
    function applyData(event) {
        event.preventDefault()
        E50Instance.applyData(event.target.dataset)
    }
    /**
    * Create the vector from the center of the selected POI to point by lon and lat
    */
    function showLayer(event) {
        const lon = parseFloat(event.target.dataset.lon)
        const lat = parseFloat(event.target.dataset.lat)
        E50Instance.createVector(lon, lat)
        E50Instance.showLayer()
    }
    /**
    * Remove all vectors and hide the layer
    */
    function hideLayer() {
        E50Instance.removeVectors()
        E50Instance.hideLayer()
    }
    /**
    * Search the city name from available in the editor area
    * @param  {String} city
    * @return {[Number,String]}
    */
    function detectCity(city) {
        // Get the list of all available cities
        let cities = E50Instance.wmeSDK.DataModel.Cities.getAll()
        .filter(city => city.name)
        // More than one city, use city with best matching score
        // Remove text in the "()"; Waze puts the region name to the pair brackets
        let best = findBestMatch(city, cities.map(city => city.name.replace(/( ?\(.*\))/gi, '')))
        if (best > -1) {
            console.info("✅ City detected")
            return [cities[best]['id'], cities[best]['name']]
            /*} else if (cities.length === 1) {
            console.info("❎ City doesn't found, uses default city")
            return [cities[0]['id'], cities[0]['name']]*/
        } else {
            console.info("❌ City doesn't found")
            return [null, city]
        }
    }
    /**
    * Search the street name from available in the editor area
    * Normalize the street name by UA rules
    * @param  {Number} cityId
    * @param  {String} street
    * @return {[Number,String]}
    */
    function detectStreet(cityId, street) {
        // It can be empty
        if (street.trim() === '') {
            return [null, null]
        }
        // Get all streets
        let streets = E50Instance.wmeSDK.DataModel.Streets.getAll()
        .filter(street => street.cityId === cityId)
        .filter(street => street.name)
        // Get type and create RegExp for filter streets
        let reTypes = new RegExp('(đường|QL|quốc lộ|QL\.|TL|TL.|tỉnh lộ|đường tỉnh|ĐT\.|hẻm|ngõ|phố|đại lộ|cầu|Đ\.|khu phố|thôn|xóm|đường huyện|ĐH|ĐH\.)', 'gi')
        let matches = [...street.matchAll(reTypes)]
        let types = []
        // Detect type(s)
        if (matches.length === 0) {
            types.push('Đ.') // set up a basic type
            street = 'Đ. ' + street
        } else {
            types = matches.map(match => match[0].toLowerCase())
        }
        // Filter streets by detected type(s)
        let filteredStreets = streets.filter(street => types.some(type => street.name.indexOf(type) > -1))
        // Matching names without type(s)
        let best = findBestMatch(
            street.replace(reTypes, '').toLowerCase().trim(),
            filteredStreets.map(street => street.name.replace(reTypes, '').toLowerCase().trim())
        )
        if (best > -1) {
            return [filteredStreets[best]['id'], filteredStreets[best]['name']]
        } else {
            return [null, street]
        }
    }
    /**
    * @link   https://github.com/aceakash/string-similarity
    * @param  {String} first
    * @param  {String} second
    * @return {Number}
    */
    function compareTwoStrings(first, second) {
        first = first.replace(/\s+/g, '')
        second = second.replace(/\s+/g, '')
        if (!first.length && !second.length) return 1           // if both are empty strings
        if (!first.length || !second.length) return 0           // if only one is empty string
        if (first === second) return 1                          // identical
        if (first.length === 1 && second.length === 1) return 0 // both are 1-letter strings
        if (first.length < 2 || second.length < 2) return 0     // if either is a 1-letter string
        let firstBigrams = new Map()
        for (let i = 0; i < first.length - 1; i++) {
            const bigram = first.substring(i, i + 2)
            const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1
            firstBigrams.set(bigram, count)
        }
        let intersectionSize = 0
        for (let i = 0; i < second.length - 1; i++) {
            const bigram = second.substring(i, i + 2)
            const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0
            if (count > 0) {
                firstBigrams.set(bigram, count - 1)
                intersectionSize++
            }
        }
        return (2.0 * intersectionSize) / (first.length + second.length - 2)
    }
    /**
    * @param  {String} mainString
    * @param  {String[]} targetStrings
    * @return {Number}
    */
    function findBestMatch(mainString, targetStrings) {
        let bestMatch = ''
        let bestMatchRating = 0
        let bestMatchIndex = -1
        for (let i = 0; i < targetStrings.length; i++) {
            let rating = compareTwoStrings(mainString, targetStrings[i])
            if (rating > bestMatchRating) {
                bestMatch = targetStrings[i]
                bestMatchRating = rating
                bestMatchIndex = i
            }
        }
        if (bestMatch === '' || bestMatchRating < 0.35) {
            console.log('❌', mainString, '🆚', targetStrings)
            return -1
        } else {
            console.log('✅', mainString, '🆚', bestMatch, ':', bestMatchRating)
            return bestMatchIndex
        }
    }
})();