WME E58 Map's previews

Create small previews for chosen map providers

目前為 2022-08-23 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME E58 Map's previews
// @version      0.3.6
// @description  Create small previews for chosen map providers
// @author       Anton Shevchuk
// @license      MIT License
// @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*
// @exclude      https://beta.waze.com/user/editor*
// @icon         data:image/png;base64,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
// @require      https://greasyfork.org/scripts/389765-common-utils/code/CommonUtils.js?version=1083313
// @require      https://greasyfork.org/scripts/389117-apihelper/code/APIHelper.js?version=1082818
// @require      https://greasyfork.org/scripts/389577-apihelperui/code/APIHelperUI.js?version=1082967
// @namespace    https://greasyfork.org/users/227648
// ==/UserScript==

/* jshint esversion: 8 */
/* global require, $, W, I18n, APIHelper, APIHelperUI, Settings, H, google */
(function () {
  'use strict'

  let helper, tab, sidebar

  const NAME = 'E58'

  // Translation
  const TRANSLATION = {
    'en': {
      // Tab title
      title: 'Maps',
      // Tab description
      description: 'Reload page for apply changes',
      maps: {
        // Fieldset's legend
        title: 'Sources',
        // Fieldset's description
        description: 'To avoid UI issues don\'t use more than two maps providers',
        // Description for option `gis`
        gis: '2GIS',
        // Description for option `Google`
        google: 'Google',
        // Description for option `HERE`
        here: 'HERE',
        // Description for option `OSM`
        osm: 'Open Street Map',
      },
      options: {
        title: 'Options',
        controls: 'Controls on the map',
        interactive: 'Interaction with the map',
      },
      position: {
        title: 'Position',
        options: {
          top: 'Top',
          bottom: 'Bottom',
        }
      },
      height: {
        title: 'Height of the map'
      },
    },
    'uk': {
      title: 'Карти',
      description: 'Оновіть сторінку після внесення змін',
      maps: {
        title: 'Джерела',
        description: 'Для запобігання проблем з інтерфейсом не використовуйте одразу більше ніж дві карти',
        gis: '2GIS',
        google: 'Google',
        here: 'HERE',
        osm: 'Open Street Map',
      },
      options: {
        title: 'Налаштування',
        controls: 'Елементи управління',
        interactive: 'Можливість взаємодіяти с картою',
      },
      position: {
        title: 'Розташування',
        options: {
          top: 'Зверху',
          bottom: 'Знизу',
        },
      },
      height: {
        title: 'Висота карти'
      },
    },
    'ru': {
      title: 'Карты',
      description: 'Обновите страницу после изменений',
      maps: {
        title: 'Источники',
        description: 'Для избежания проблем с интерфейсом не используйте больше двух карт',
        gis: '2GIS',
        google: 'Google',
        here: 'HERE',
        osm: 'Open Street Map',
      },
      options: {
        title: 'Настройки',
        controls: 'Элементи управления карты',
        interactive: 'Возможность взаимодествия с картой',
      },
      position: {
        title: 'Позиция панели',
        options: {
          top: 'Вверху',
          bottom: 'Внизу',
        },
      },
      height: {
        title: 'Высота элемента'
      },
    }
  }

  // Default settings
  const settings = {
    maps: {
      gis: false,
      google: false,
      here: false,
      osm: false,
    },
    options: {
      controls: false,
      interactive: false,
    },
    position: 'bottom',
    height: 200,
  }

  const position = [
    'top', 'bottom'
  ]

  const height = {
    min: 100, max: 250, value: 200, step: 10
  }

  let ScriptSettings = new Settings(NAME, settings)

  APIHelper.bootstrap()
  APIHelper.addTranslation(NAME, TRANSLATION)
  APIHelper.addStyle(
    '#sidebar #links:before { display: none; }' +
    '#E58-map-container { max-height: 50vh; }' +
    '.e58 legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
    '.e58 fieldset { border: 1px solid #ddd; padding: 4px; }' +
    '.e58 fieldset p { padding: 0; margin: 0 8px !important; }' +
    '.e58 fieldset.e58 div.controls label { white-space: normal; font-weight: 400; }' +
    '.e58 .e58-height input { margin-bottom: 4px }' +
    '.e58 .e58-height label { display:block; text-align: center }' +
    '.e58 .e58-height label::after { margin: 0 0 0 8px; padding: 4px; border: 0; }' +
    ''
  )

  /**
   * Basic Map class
   */
  class MapPreview {
    constructor (uid, container) {
      this.uid = uid
      this.map = null
      this.wrapper = this._wrapper()
      container.append(this.wrapper)

      this.controls = ScriptSettings.get('options').controls
      this.interactive = ScriptSettings.get('options').interactive
    }

    /**
     * Load external JS Map library
     * @param  {String} url
     * @return {Promise<*>}
     */
    async script (url) {
      this.wrapper.style.height = ScriptSettings.get('height') + 'px'
      return await $.ajax({
        url: url,
        cache: true,
        dataType: 'script',
        success: () => console.log(NAME, this.uid, 'loaded')
      })
    }

    /**
     * Build div for map
     * @return {HTMLDivElement}
     * @protected
     */
    _wrapper () {
      let div = document.createElement('div')
      div.id = this._uid()
      return div
    }

    _uid () {
      return NAME + '-map-' + this.uid
    }

    _center () {
      return W.map.getCenter().transform('EPSG:900913', 'EPSG:4326')
    }

    _zoom () {
      return W.map.getZoom()
    }

    update () {
      let center = this._center()
      this._update(center.lat, center.lon, this._zoom())
    }

    _update (lat, lon, zoom) {
      throw new Error('Abstract method')
    }
  }

  /**
   * 2Gis
   */
  class GisPreview extends MapPreview {
    constructor (container) {
      super('2Gis', container)
    }

    async render () {
      await this.script('https://maps.api.2gis.ua/2.0/loader.js?pkg=basic')
      let pos = this._center()
      DG.then(() => {
        this.map = DG.map(this._uid(), {
          center: [pos.lat, pos.lon],
          zoom: this._zoom(),
          fullscreenControl: this.controls,
          zoomControl: this.controls,
          boxZoom: this.controls,
          doubleClickZoom: this.interactive,
          scrollWheelZoom: this.interactive,
          dragging: this.interactive,
          keyboard: this.interactive,
          currentLang: 'ru' // language interface
        })
        // Setup handler
        W.map.events.register('moveend', null, () => this.update())
      })
    }

    _update (lat, lon, zoom) {
      this.map.setZoom(zoom)
      this.map.panTo([lat, lon])
    }
  }

  /**
   * Google Maps
   */
  class GooglePreview extends MapPreview {
    constructor (container) {
      super('Google', container)
    }

    async render () {
      let pos = this._center()
      let container = document.getElementById(this._uid())
      container.style.height = ScriptSettings.get('height') + 'px'
      this.map = new google.maps.Map(container, {
        center: new google.maps.LatLng(pos.lat, pos.lon),
        zoom: this._zoom(),
        mapTypeId: 'roadmap',
        mapTypeControl: false,
        streetViewControl: false,
        disableDefaultUI: !this.controls,
        gestureHandling: this.interactive ? 'cooperative ' : 'none',
        zoomControl: this.controls,
      })

      // Setup handler
      W.map.events.register('moveend', null, () => this.update())
    }

    _update (lat, lon, zoom) {
      this.map.setZoom(zoom)
      this.map.setCenter(new google.maps.LatLng(lat, lon))
    }
  }

  /**
   * Open Street Maps
   */
  class OSMPreview extends MapPreview {
    constructor (container) {
      super('OSM', container)
    }

    async render () {
      let pos = this._center()
      let container = document.getElementById(this._uid())
      container.style.height = ScriptSettings.get('height') + 'px'
      this.map = new google.maps.Map(container, {
        center: new google.maps.LatLng(pos.lat, pos.lon),
        zoom: this._zoom(),
        mapTypeId: 'OSM',
        mapTypeControl: false,
        streetViewControl: false,
        disableDefaultUI: !this.controls,
        gestureHandling: this.interactive ? 'cooperative ' : 'none',
        zoomControl: this.controls,
      })

      // Define OSM map type pointing at the OpenStreetMap tile server
      this.map.mapTypes.set('OSM', new google.maps.ImageMapType({
        getTileUrl: function (coord, zoom) {
          return 'https://tile.openstreetmap.org/' + zoom + '/' + coord.x + '/' + coord.y + '.png'
        },
        tileSize: new google.maps.Size(256, 256),
        name: 'OpenStreetMap',
        maxZoom: 18
      }))

      // Setup handler
      W.map.events.register('moveend', null, () => this.update())
    }

    _update (lat, lon, zoom) {
      this.map.setZoom(zoom)
      this.map.setCenter(new google.maps.LatLng(lat, lon))
    }
  }

  class HerePreview extends MapPreview {
    constructor (container) {
      super('Here', container)
    }

    async render () {
      await this.script('https://js.api.here.com/v3/3.1/mapsjs-core.js')
      await this.script('https://js.api.here.com/v3/3.1/mapsjs-service.js')

      let pos = this._center()

      // Initialize the platform object:
      var platform = new H.service.Platform({
        'apikey': 'vmj30nPbru3jmJdcln4' + '-wJe-' + 'w3BH6CrCxHZaku8UbY4'
      })

      // Obtain the default map types from the platform object
      let maptypes = platform.createDefaultLayers()

      // Instantiate (and display) a map object:
      this.map = new H.Map(
        document.getElementById(this._uid()),
        maptypes.vector.normal.map,
        {
          zoom: this._zoom(),
          center: { lng: pos.lon, lat: pos.lat }
        })
      // Setup handler
      W.map.events.register('moveend', null, () => this.update())
    }

    _update (lat, lon, zoom) {
      this.map.setZoom(zoom)
      this.map.setCenter(new H.geo.Point(lat, lon))
    }
  }

  // Handlers
  $(document).on('init.apihelper', ready)
  $(window).on('beforeunload', () => ScriptSettings.save())

  function ready () {
    // Setup Tab with options
    helper = new APIHelperUI(NAME)
    tab = helper.createTab(
      I18n.t(NAME).title,
      I18n.t(NAME).description,
      '<i class="w-icon panel-header-component-icon w-icon-map"></i>'
    )

    // Setup providers map settings
    let fsMap = helper.createFieldset(I18n.t(NAME).maps.title, I18n.t(NAME).maps.description)
    let maps = ScriptSettings.get('maps')
    for (let item in maps) {
      if (maps.hasOwnProperty(item)) {
        fsMap.addCheckbox('maps-' + item, I18n.t(NAME).maps[item], I18n.t(NAME).maps[item], function (event) {
          ScriptSettings.set(['maps', item], event.target.checked)
        }, ScriptSettings.get('maps', item))
      }
    }
    tab.addElement(fsMap)

    // Setup options for maps
    let fsOptions = helper.createFieldset(I18n.t(NAME).options.title)
    let options = ScriptSettings.get('options')
    for (let item in options) {
      if (options.hasOwnProperty(item)) {
        fsOptions.addCheckbox('options-' + item, I18n.t(NAME).options[item], I18n.t(NAME).options[item], function (event) {
          ScriptSettings.set(['options', item], event.target.checked)
        }, ScriptSettings.get('options', item))
      }
    }
    tab.addElement(fsOptions)

    // Setup position and height options for maps
    let fsPosition = helper.createFieldset(I18n.t(NAME).position.title)
    position.forEach(value => {
      fsPosition.addRadio('position', I18n.t(NAME).position.options[value], I18n.t(NAME).position.options[value], function (event) {
        if (event.target.checked) {
          ScriptSettings.set(['position'], event.target.value)
        }
      }, value, ScriptSettings.get('position') === value)
    })

    fsPosition.addRange('height', I18n.t(NAME).height.title, I18n.t(NAME).height.title, function (event) {
      ScriptSettings.set(['height'], event.target.value)
      addHeightStyle(event.target.value)
    }, height.min, height.max, ScriptSettings.get('height'), height.step)
    addHeightStyle(ScriptSettings.get('height'))

    tab.addElement(fsPosition)
    tab.inject()

    // Setup Preview Map element
    sidebar = document.createElement('div')
    sidebar.id = NAME + '-map-container'
    sidebar.className = 'flex-noshrink'

    // Injection
    let content = document.getElementById('sidebarContent')
    if (ScriptSettings.get('position') === 'top') {
      document.getElementById('sidebar').insertBefore(sidebar, content)
    } else {
      document.getElementById('sidebar').insertBefore(sidebar, content.nextSibling)
    }

    if (ScriptSettings.get('maps').gis) {
      let Gis = new GisPreview(sidebar)
      Gis.render()
    }
    if (ScriptSettings.get('maps').google) {
      let Google = new GooglePreview(sidebar)
      Google.render()
    }
    if (ScriptSettings.get('maps').here) {
      let Here = new HerePreview(sidebar)
      Here.render()
    }
    if (ScriptSettings.get('maps').osm) {
      let OSM = new OSMPreview(sidebar)
      OSM.render()
    }
  }

  function addHeightStyle (height) {
    let style = document.createElement('style')
    style.innerText = '.e58 .e58-height label::after { content: "' + height + '" }'
    document.head.appendChild(style)
  }
})()