WME WazeMY

WazeMY beta

目前為 2025-01-01 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        WME WazeMY
// @namespace   https://www.github.com/junyian/
// @version     2025.01.01.01
// @author      junyianl <[email protected]>
// @source      https://github.com/junyian/wme-wazemy
// @license     MIT
// @match       *://www.waze.com/editor*
// @match       *://www.waze.com/*/editor*
// @require     https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require     https://greasyfork.org/scripts/449165-wme-wazemy-trafcamlist/code/wme-wazemy-trafcamlist.js
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlHttpRequest
// @connect     p3.fgies.com
// @connect     p4.fgies.com
// @connect     t2.fgies.com
// @connect     jalanow.com
// @connect     llm.gov.my
// @run-at      document-end
// @description WazeMY beta
// ==/UserScript==

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/style/main.less":
/***/ ((module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   A: () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./node_modules/css-loader/dist/runtime/noSourceMaps.js");
/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js");
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);
// Imports


var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
// Module
___CSS_LOADER_EXPORT___.push([module.id, `.wazemySettings {
  border: 1px solid;
  padding: 8px;
  border-radius: 4px;
}
.wazemySettings legend {
  margin-bottom: 0px;
  border-bottom-style: none;
  width: auto;
}
.wazemySettings h6 {
  margin-bottom: 0px;
}
.wazemySettings input {
  margin-top: 0px;
}
#wazemyTooltip {
  height: auto;
  width: auto;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  border-radius: 4px;
  padding: 4px;
  position: absolute;
  top: 0px;
  left: 0px;
  visibility: hidden;
  z-index: 10000;
}
#wazemyPlaces_table {
  overflow-x: scroll;
}
#wazemyPlaces_venues {
  width: 95%;
  border: 1px solid;
}
#wazemyPlaces_venues th {
  border: 1px solid;
  background-color: #ccc;
}
#wazemyPlaces_venues td {
  border: 1px solid;
}
`, ""]);
// Exports
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);


/***/ }),

/***/ "./node_modules/css-loader/dist/runtime/api.js":
/***/ ((module) => {



/*
  MIT License http://www.opensource.org/licenses/mit-license.php
  Author Tobias Koppers @sokra
*/
module.exports = function (cssWithMappingToString) {
  var list = [];

  // return the list of modules as css string
  list.toString = function toString() {
    return this.map(function (item) {
      var content = "";
      var needLayer = typeof item[5] !== "undefined";
      if (item[4]) {
        content += "@supports (".concat(item[4], ") {");
      }
      if (item[2]) {
        content += "@media ".concat(item[2], " {");
      }
      if (needLayer) {
        content += "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {");
      }
      content += cssWithMappingToString(item);
      if (needLayer) {
        content += "}";
      }
      if (item[2]) {
        content += "}";
      }
      if (item[4]) {
        content += "}";
      }
      return content;
    }).join("");
  };

  // import a list of modules into the list
  list.i = function i(modules, media, dedupe, supports, layer) {
    if (typeof modules === "string") {
      modules = [[null, modules, undefined]];
    }
    var alreadyImportedModules = {};
    if (dedupe) {
      for (var k = 0; k < this.length; k++) {
        var id = this[k][0];
        if (id != null) {
          alreadyImportedModules[id] = true;
        }
      }
    }
    for (var _k = 0; _k < modules.length; _k++) {
      var item = [].concat(modules[_k]);
      if (dedupe && alreadyImportedModules[item[0]]) {
        continue;
      }
      if (typeof layer !== "undefined") {
        if (typeof item[5] === "undefined") {
          item[5] = layer;
        } else {
          item[1] = "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {").concat(item[1], "}");
          item[5] = layer;
        }
      }
      if (media) {
        if (!item[2]) {
          item[2] = media;
        } else {
          item[1] = "@media ".concat(item[2], " {").concat(item[1], "}");
          item[2] = media;
        }
      }
      if (supports) {
        if (!item[4]) {
          item[4] = "".concat(supports);
        } else {
          item[1] = "@supports (".concat(item[4], ") {").concat(item[1], "}");
          item[4] = supports;
        }
      }
      list.push(item);
    }
  };
  return list;
};

/***/ }),

/***/ "./node_modules/css-loader/dist/runtime/noSourceMaps.js":
/***/ ((module) => {



module.exports = function (i) {
  return i[1];
};

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
/***/ ((module) => {



var stylesInDOM = [];
function getIndexByIdentifier(identifier) {
  var result = -1;
  for (var i = 0; i < stylesInDOM.length; i++) {
    if (stylesInDOM[i].identifier === identifier) {
      result = i;
      break;
    }
  }
  return result;
}
function modulesToDom(list, options) {
  var idCountMap = {};
  var identifiers = [];
  for (var i = 0; i < list.length; i++) {
    var item = list[i];
    var id = options.base ? item[0] + options.base : item[0];
    var count = idCountMap[id] || 0;
    var identifier = "".concat(id, " ").concat(count);
    idCountMap[id] = count + 1;
    var indexByIdentifier = getIndexByIdentifier(identifier);
    var obj = {
      css: item[1],
      media: item[2],
      sourceMap: item[3],
      supports: item[4],
      layer: item[5]
    };
    if (indexByIdentifier !== -1) {
      stylesInDOM[indexByIdentifier].references++;
      stylesInDOM[indexByIdentifier].updater(obj);
    } else {
      var updater = addElementStyle(obj, options);
      options.byIndex = i;
      stylesInDOM.splice(i, 0, {
        identifier: identifier,
        updater: updater,
        references: 1
      });
    }
    identifiers.push(identifier);
  }
  return identifiers;
}
function addElementStyle(obj, options) {
  var api = options.domAPI(options);
  api.update(obj);
  var updater = function updater(newObj) {
    if (newObj) {
      if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap && newObj.supports === obj.supports && newObj.layer === obj.layer) {
        return;
      }
      api.update(obj = newObj);
    } else {
      api.remove();
    }
  };
  return updater;
}
module.exports = function (list, options) {
  options = options || {};
  list = list || [];
  var lastIdentifiers = modulesToDom(list, options);
  return function update(newList) {
    newList = newList || [];
    for (var i = 0; i < lastIdentifiers.length; i++) {
      var identifier = lastIdentifiers[i];
      var index = getIndexByIdentifier(identifier);
      stylesInDOM[index].references--;
    }
    var newLastIdentifiers = modulesToDom(newList, options);
    for (var _i = 0; _i < lastIdentifiers.length; _i++) {
      var _identifier = lastIdentifiers[_i];
      var _index = getIndexByIdentifier(_identifier);
      if (stylesInDOM[_index].references === 0) {
        stylesInDOM[_index].updater();
        stylesInDOM.splice(_index, 1);
      }
    }
    lastIdentifiers = newLastIdentifiers;
  };
};

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/insertBySelector.js":
/***/ ((module) => {



var memo = {};

/* istanbul ignore next  */
function getTarget(target) {
  if (typeof memo[target] === "undefined") {
    var styleTarget = document.querySelector(target);

    // Special case to return head of iframe instead of iframe itself
    if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {
      try {
        // This will throw an exception if access to iframe is blocked
        // due to cross-origin restrictions
        styleTarget = styleTarget.contentDocument.head;
      } catch (e) {
        // istanbul ignore next
        styleTarget = null;
      }
    }
    memo[target] = styleTarget;
  }
  return memo[target];
}

/* istanbul ignore next  */
function insertBySelector(insert, style) {
  var target = getTarget(insert);
  if (!target) {
    throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
  }
  target.appendChild(style);
}
module.exports = insertBySelector;

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/insertStyleElement.js":
/***/ ((module) => {



/* istanbul ignore next  */
function insertStyleElement(options) {
  var element = document.createElement("style");
  options.setAttributes(element, options.attributes);
  options.insert(element, options.options);
  return element;
}
module.exports = insertStyleElement;

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js":
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {



/* istanbul ignore next  */
function setAttributesWithoutAttributes(styleElement) {
  var nonce =  true ? __webpack_require__.nc : 0;
  if (nonce) {
    styleElement.setAttribute("nonce", nonce);
  }
}
module.exports = setAttributesWithoutAttributes;

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/styleDomAPI.js":
/***/ ((module) => {



/* istanbul ignore next  */
function apply(styleElement, options, obj) {
  var css = "";
  if (obj.supports) {
    css += "@supports (".concat(obj.supports, ") {");
  }
  if (obj.media) {
    css += "@media ".concat(obj.media, " {");
  }
  var needLayer = typeof obj.layer !== "undefined";
  if (needLayer) {
    css += "@layer".concat(obj.layer.length > 0 ? " ".concat(obj.layer) : "", " {");
  }
  css += obj.css;
  if (needLayer) {
    css += "}";
  }
  if (obj.media) {
    css += "}";
  }
  if (obj.supports) {
    css += "}";
  }
  var sourceMap = obj.sourceMap;
  if (sourceMap && typeof btoa !== "undefined") {
    css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */");
  }

  // For old IE
  /* istanbul ignore if  */
  options.styleTagTransform(css, styleElement, options.options);
}
function removeStyleElement(styleElement) {
  // istanbul ignore if
  if (styleElement.parentNode === null) {
    return false;
  }
  styleElement.parentNode.removeChild(styleElement);
}

/* istanbul ignore next  */
function domAPI(options) {
  if (typeof document === "undefined") {
    return {
      update: function update() {},
      remove: function remove() {}
    };
  }
  var styleElement = options.insertStyleElement(options);
  return {
    update: function update(obj) {
      apply(styleElement, options, obj);
    },
    remove: function remove() {
      removeStyleElement(styleElement);
    }
  };
}
module.exports = domAPI;

/***/ }),

/***/ "./node_modules/style-loader/dist/runtime/styleTagTransform.js":
/***/ ((module) => {



/* istanbul ignore next  */
function styleTagTransform(css, styleElement) {
  if (styleElement.styleSheet) {
    styleElement.styleSheet.cssText = css;
  } else {
    while (styleElement.firstChild) {
      styleElement.removeChild(styleElement.firstChild);
    }
    styleElement.appendChild(document.createTextNode(css));
  }
}
module.exports = styleTagTransform;

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			id: moduleId,
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/compat get default export */
/******/ 	(() => {
/******/ 		// getDefaultExport function for compatibility with non-harmony modules
/******/ 		__webpack_require__.n = (module) => {
/******/ 			var getter = module && module.__esModule ?
/******/ 				() => (module['default']) :
/******/ 				() => (module);
/******/ 			__webpack_require__.d(getter, { a: getter });
/******/ 			return getter;
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/nonce */
/******/ 	(() => {
/******/ 		__webpack_require__.nc = undefined;
/******/ 	})();
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {

// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
var injectStylesIntoStyleTag = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
var injectStylesIntoStyleTag_default = /*#__PURE__*/__webpack_require__.n(injectStylesIntoStyleTag);
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleDomAPI.js
var styleDomAPI = __webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js");
var styleDomAPI_default = /*#__PURE__*/__webpack_require__.n(styleDomAPI);
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertBySelector.js
var insertBySelector = __webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js");
var insertBySelector_default = /*#__PURE__*/__webpack_require__.n(insertBySelector);
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js
var setAttributesWithoutAttributes = __webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
var setAttributesWithoutAttributes_default = /*#__PURE__*/__webpack_require__.n(setAttributesWithoutAttributes);
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertStyleElement.js
var insertStyleElement = __webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js");
var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleElement);
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleTagTransform.js
var styleTagTransform = __webpack_require__("./node_modules/style-loader/dist/runtime/styleTagTransform.js");
var styleTagTransform_default = /*#__PURE__*/__webpack_require__.n(styleTagTransform);
// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/style/main.less
var main = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/style/main.less");
;// CONCATENATED MODULE: ./src/style/main.less

      
      
      
      
      
      
      
      
      

var options = {};

options.styleTagTransform = (styleTagTransform_default());
options.setAttributes = (setAttributesWithoutAttributes_default());
options.insert = insertBySelector_default().bind(null, "head");
options.domAPI = (styleDomAPI_default());
options.insertStyleElement = (insertStyleElement_default());

var update = injectStylesIntoStyleTag_default()(main/* default */.A, options);




       /* harmony default export */ const style_main = (main/* default */.A && main/* default */.A.locals ? main/* default */.A.locals : undefined);

;// CONCATENATED MODULE: ./src/SettingsStorage.ts
class SettingsStorage {
    /**
     * Initializes a new instance of the SettingsStorage class with the specified storage key.
     *
     * @param {string} storageKey - The key used to store the settings in the local storage.
     */
    constructor(storageKey) {
        this.storageKey = storageKey;
    }
    /**
     * Saves the given settings to the local storage.
     *
     * @param {any} settings - The settings to be saved.
     * @return {void} This function does not return anything.
     */
    saveSettings(settings) {
        localStorage.setItem(this.storageKey, JSON.stringify(settings));
    }
    /**
     * Loads the settings from the local storage.
     *
     * @return {any} The loaded settings, or null if no settings are found.
     */
    loadSettings() {
        const settings = localStorage.getItem(this.storageKey);
        return settings ? JSON.parse(settings) : null;
    }
    /**
     * Updates a specific setting in the local storage.
     *
     * @param {string} key - The key of the setting to update.
     * @param {any} value - The new value for the setting.
     * @return {void} This function does not return anything.
     */
    updateSetting(key, value) {
        const settings = this.loadSettings() || {};
        settings[key] = value;
        this.saveSettings(settings);
    }
    /**
     * Retrieves a specific setting from the local storage.
     *
     * @param {string} key - The key of the setting to retrieve.
     * @return {any} The value of the setting, or null if the setting is not found.
     */
    getSetting(key) {
        const settings = this.loadSettings();
        return settings ? settings[key] : null;
    }
    /**
     * Removes a specific setting from the local storage.
     *
     * @param {string} key - The key of the setting to remove.
     * @return {void} This function does not return anything.
     */
    removeSetting(key) {
        const settings = this.loadSettings();
        if (settings && key in settings) {
            delete settings[key];
            this.saveSettings(settings);
        }
    }
}
SettingsStorage.instance = new SettingsStorage("WME_wazemySettings");

;// CONCATENATED MODULE: ./src/plugins/PluginTooltip.ts


class PluginTooltip {
    constructor() {
        this.initialize();
    }
    /**
     * Initializes the plugin by adding settings into the tab pane, setting the initial state of the settings based on the last stored value, and adding a hidden tooltip window.
     *
     * @return {void} This function does not return anything.
     */
    initialize() {
        // Add settings into tab pane.
        const settingsHTML = `<input type="checkbox" id="wazemySettings_tooltip_enable"/>
<label for="wazemySettings_tooltip_enable">Enable map tooltip</label>`;
        $("#wazemySettings_settings").append(settingsHTML);
        $("#wazemySettings_tooltip_enable").on("change", () => {
            PluginManager.instance.updatePluginSettings("tooltip", {
                enable: $("#wazemySettings_tooltip_enable").prop("checked"),
            });
        });
        // Set settings according to last stored value.
        const savedSettings = SettingsStorage.instance.getSetting("tooltip");
        if (savedSettings?.enable === true) {
            $("#wazemySettings_tooltip_enable").prop("checked", true);
        }
        else {
            $("#wazemySettings_tooltip_enable").prop("checked", false);
        }
        // Add hidden tooltip window.
        const tooltipHTML = `<div id="wazemyTooltip"></div>`;
        $(document.body).append(tooltipHTML);
        console.log("[WazeMY] PluginTooltip initialized.");
    }
    /**
     * Enables the PluginTooltip by registering the "mousemove" event, showing the tooltip, and logging a message.
     *
     * @return {void} This function does not return anything.
     */
    enable() {
        WazeWrap.Events.register("mousemove", null, this.showTooltip);
        $("#wazemyTooltip").show();
        console.log("[WazeMY] PluginTooltip enabled.");
    }
    /**
     * Disables the PluginTooltip by unregistering the "mousemove" event, hiding the tooltip, and logging a message.
     *
     * @return {void} This function does not return anything.
     */
    disable() {
        WazeWrap.Events.unregister("mousemove", null, this.showTooltip);
        $("#wazemyTooltip").hide();
        console.log("[WazeMY] PluginTooltip disabled.");
    }
    /**
     * Updates the settings of the PluginTooltip based on the provided settings object.
     *
     * @param {any} settings - The new settings object.
     * @return {void} This function does not return anything.
     */
    updateSettings(settings) {
        if (settings.enable === true) {
            this.enable();
        }
        else {
            this.disable();
        }
        console.log("[WazeMY] PluginTooltip settings updated.", settings);
    }
    /**
     * Shows the tooltip at the mouse position.
     *
     * @param {MouseEvent} e - The mouse event.
     * @return {void} This function does not return anything.
     */
    showTooltip(e) {
        let output = "";
        let showTooltip = false;
        // Manual check of settings because unregistering event is not working.
        if ($("#wazemySettings_tooltip_enable").prop("checked") === true) {
            const landmark = W.map.venueLayer.getFeatureBy("renderIntent", "highlight");
            const segment = W.map.segmentLayer.getFeatureBy("renderIntent", "highlight");
            if (landmark) {
                output = `<b>${landmark.attributes.wazeFeature._wmeObject.attributes.name}</b><br>`;
                const categories = landmark.attributes.wazeFeature._wmeObject.getCategories();
                output += `<i>[${categories.join(", ")}]</i><br>`;
                const address = landmark.attributes.wazeFeature._wmeObject.getAddress();
                try {
                    output += address.getHouseNumber()
                        ? `${address.getHouseNumber()}, `
                        : "";
                    output += address.getStreetName()
                        ? `${address.getStreetName()}<br>`
                        : `No street<br>`;
                    output += `${address.getCityName()}, `;
                    output += `${address.getStateName()}<br>`;
                }
                catch {
                    output += "No address<br>";
                }
                output += `<b>Lock:</b> ${landmark.attributes.wazeFeature._wmeObject.getLockRank() + 1}`;
                showTooltip = true;
            }
            else if (segment) {
                const segmentId = segment.attributes.wazeFeature.id;
                const address = segment.attributes.wazeFeature._wmeObject.getAddress();
                output = `<b>${address.getStreetName()}</b><br>`;
                const altStreets = address.getAltStreets();
                for (let i = 0; i < altStreets.length; i++) {
                    const altStreetName = altStreets[i].getStreetName();
                    output += `Alt: ${altStreetName}<br>`;
                }
                output += `${address.getCityName()}, ${address.getStateName()}<br>`;
                output += `<b>ID:</b> ${segmentId}<br>`;
                output += `<b>Lock:</b> ${segment.attributes.wazeFeature._wmeObject.getLockRank() + 1}`;
                showTooltip = true;
            }
            const tooltipDiv = $("#wazemyTooltip");
            if (showTooltip === true) {
                const tw = tooltipDiv.innerWidth();
                const th = tooltipDiv.innerHeight();
                let tooltipX = e.clientX + window.scrollX + 15;
                let tooltipY = e.clientY + window.scrollY + 15;
                // Handle cases where tooltip is too near the edge.
                if (tooltipX + tw > W.map.$map.innerWidth()) {
                    tooltipX -= tw + 20; // 20 = scroll bar size
                    if (tooltipX < 0) {
                        tooltipX = 0;
                    }
                }
                if (tooltipY + th > W.map.$map.innerHeight()) {
                    tooltipY -= th + 20;
                    if (tooltipY < 0) {
                        tooltipY = 0;
                    }
                }
                tooltipDiv.html(output);
                tooltipDiv.css("top", `${tooltipY}px`);
                tooltipDiv.css("left", `${tooltipX}px`);
                tooltipDiv.css("visibility", "visible");
            }
            else {
                tooltipDiv.css("visibility", "hidden");
            }
        }
    }
}

;// CONCATENATED MODULE: ./src/plugins/PluginCopyLatLon.ts
class PluginCopyLatLon {
    constructor() {
        this.initialize();
    }
    /**
     * Initialize plugin.
     *
     * @return {void} This function does not return anything.
     */
    initialize() {
        const settingsHTML = `<div>Ctrl+Alt+C: <i>Copy lat/lon of mouse position to clipboard.</i></div>`;
        $("#wazemySettings_shortcuts").append(settingsHTML);
        this.enable(); // Manually enable plugin since there is no settings to trigger this.
        console.log("[WazeMY] PluginCopyLatLon initialized.");
    }
    /**
     * Enable plugin.
     *
     * @return {void} This function does not return anything.
     */
    enable() {
        new WazeWrap.Interface.Shortcut("WazeMY_latloncopy", "Copies lat/lon of mouse position to clipboard.", "wazemy", "WazeMY", "CA+c", this.copyLatLon, null).add();
        console.log("[WazeMY] PluginCopyLatLon enabled.");
    }
    /**
     * Disable plugin.
     *
     * @return {void} This function does not return anything.
     */
    disable() {
        console.log("[WazeMY] PluginCopyLatLon disabled.");
    }
    /**
     * Updates the settings of the PluginCopyLatLon based on the provided settings object.
     *
     * @return {void} This function does not return anything.
     */
    updateSettings(settings) {
        console.log("[WazeMY] PluginCopyLatLon settings updated.", settings);
    }
    /**
     * Copies lat/lon of mouse position to clipboard.
     *
     * @return {void} This function does not return anything.
     */
    copyLatLon() {
        const latlon = $(".wz-map-ol-control-span-mouse-position").text();
        navigator.clipboard.writeText(latlon);
    }
}

;// CONCATENATED MODULE: ./src/plugins/PluginTrafficCameras.ts


class PluginTrafficCameras {
    constructor() {
        this.initialize();
    }
    initialize() {
        // Add settings into view.
        const settingsHTML = `<div>
      <input type="checkbox" id="wazemySettings_trafcam_enable" style="margin-top:0px"/>
      <label for="wazemySettings_trafcam_enable">Traffic cameras</label><br></div>`;
        const wazemySettings = document.getElementById("wazemySettings_settings");
        $("#wazemySettings_settings").append(settingsHTML);
        // wazemySettings.insertAdjacentHTML("afterbegin", settingsHTML);
        const settingsEl = document.getElementById(`wazemySettings_trafcam_enable`);
        const savedSettings = SettingsStorage.instance.getSetting("trafcam");
        if (savedSettings?.enable === true) {
            settingsEl.checked = true;
        }
        else {
            settingsEl.checked = false;
        }
        settingsEl.onchange = (e) => {
            const target = e.target;
            PluginManager.instance.updatePluginSettings("trafcam", {
                enable: target.checked,
            });
        };
        // Install camera icon
        if (!OpenLayers.Icon) {
            this.installIconClass();
        }
        this.trafcamLayer = new OpenLayers.Layer.Markers("wazemyTrafcamLayer");
        W.map.addLayer(this.trafcamLayer);
        this.showIcons();
        console.log("PluginTrafficCameras initialized.");
    }
    enable() {
        console.log("PluginTrafficCameras enabled.");
        this.trafcamLayer.setVisibility(true);
    }
    disable() {
        console.log("PluginTrafficCameras disabled.");
        this.trafcamLayer.setVisibility(false);
    }
    updateSettings(settings) {
        if (settings.enable === true) {
            this.enable();
        }
        else {
            this.disable();
        }
        console.log("PluginTrafficCameras settings updated", settings);
    }
    installIconClass() {
        OpenLayers.Icon = OpenLayers.Class({
            url: null,
            size: null,
            offset: null,
            calculateOffset: null,
            imageDiv: null,
            px: null,
            initialize: function (url, size, offset, calculateOffset) {
                this.url = url;
                this.size = size || { w: 20, h: 20 };
                this.offset = offset || {
                    x: -(this.size.w / 2),
                    y: -(this.size.h / 2),
                };
                this.calculateOffset = calculateOffset;
                url = OpenLayers.Util.createUniqueID("OL_Icon_");
                const div = (this.imageDiv = OpenLayers.Util.createAlphaImageDiv(url));
                $(div.firstChild).removeClass("olAlphaImg"); // LEAVE THIS LINE TO PREVENT WME-HARDHATS SCRIPT FROM TURNING ALL ICONS INTO HARDHAT WAZERS --MAPOPMATIC
            },
            destroy: function () {
                this.erase();
                OpenLayers.Event.stopObservingElement(this.imageDiv.firstChild);
                this.imageDiv.innerHTML = "";
                this.imageDiv = null;
            },
            clone: function () {
                return new OpenLayers.Icon(this.url, this.size, this.offset, this.calculateOffset);
            },
            setSize: function (size) {
                null !== size && (this.size = size);
                this.draw();
            },
            setUrl: function (url) {
                null !== url && (this.url = url);
                this.draw();
            },
            draw: function (a) {
                OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, this.size, this.url, "absolute");
                this.moveTo(a);
                return this.imageDiv;
            },
            erase: function () {
                null !== this.imageDiv &&
                    null !== this.imageDiv.parentNode &&
                    OpenLayers.Element.remove(this.imageDiv);
            },
            setOpacity: function (a) {
                OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, null, null, null, null, null, a);
            },
            moveTo: function (a) {
                null !== a && (this.px = a);
                null !== this.imageDiv &&
                    (null === this.px
                        ? this.display(!1)
                        : (this.calculateOffset &&
                            (this.offset = this.calculateOffset(this.size)),
                            OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, {
                                x: this.px.x + this.offset.x,
                                y: this.px.y + this.offset.y,
                            })));
            },
            display: function (a) {
                this.imageDiv.style.display = a ? "" : "none";
            },
            isDrawn: function () {
                return (this.imageDiv &&
                    this.imageDiv.parentNode &&
                    11 != this.imageDiv.parentNode.nodeType);
            },
            CLASS_NAME: "OpenLayers.Icon",
        });
    }
    showIcons() {
        trafficCamsData.forEach((e, idx) => {
            this.drawCamIcon({
                idx: idx,
                desc: e.desc,
                src: e.url,
                width: 20,
                height: 20,
                lat: e.lat,
                lon: e.lon,
            });
        });
    }
    drawCamIcon(spec) {
        const camIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAAGXcA1uAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpBRDNGNTkwRTYzQThFMzExQTc4MDhDNjAwODdEMzdEQSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2OUI0RUEyN0IwRjcxMUUzOERFM0E1OTJCRUY3NTFBOCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2OUI0RUEyNkIwRjcxMUUzOERFM0E1OTJCRUY3NTFBOCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZGOEJBMzExNkZCMEUzMTFCOEY5QTU3QUQxM0M2MjI5IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkFEM0Y1OTBFNjNBOEUzMTFBNzgwOEM2MDA4N0QzN0RBIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+TV0cjwAABbhJREFUeNpiYIAAXiZGCIMPiP//f8DwnwnI+PT/HZD8y8AAEEBQVQzTwOSfuwz/u6qAyh4y/Gf8f4/hPwMnUJSZgQEggMCyzpZAmbsILMTHcAokPhPEWT8dqJoBgvetAtMMDBIiDCf/P4bqeMXwf+t8BiOAAGJAAqVA7A1iPH/+goFBT4OB8f9LJDueAzFQgOHGLoTZYEGgpJgww0+G/68ZQArAEsryEHpRN5BuK4FIcHMhjJORVTvBYG3MwPD2CkKwKAVI/3nBABBAYOcY6zKAjGSQEmP4cGkXxMlgK4BeaCll+B/lzzDh/yegmr8vIO4HGv/l/0eG/w4WDP9fnUM4Dhl/uMzwf/6CFQ4g9RUgE3ctRvgGGSvJMfw/tw3i7sY8hv8sQMGrQE8yuFoBZe8CeRwMDIKaDAyWRgwM2xYD+exA/AuIvwAV3kaE6sSPQCuBHv2vqczwf0YL0MR7SE4CsgsTGP5///aSCZQSGDRVGPJfv2dgNDNkcP30heHbB6BpyzYyMCRVMjCwqDIcOX+LgTEtioGRhfn/P4AAbJTfK0NhGMe/Z+ecpVkrScnIlCiWkoulpFyhxgXKXCGK3XGBG/4BJcoNN665tqsxo6XshiKtyQybn82vMZPF63nPOXKYi+fieU7P8z59P9/n6NlBVNrRRDFPMUlRIGhmM0gWzk5NSCFMuDE2MqAaUJGUhDgOgNmKkXmLwMRudA11diynI9lSKhEHG+oBu9q1mHkDf9AWWkM0dgHEb4H+PqqkKD51uxpJuSoDHpIfAowyohyUveJH+9pqUjigav/9UnIfzO/frkRvBxUuwcpK/gc3PkzfzynuwRoanYuStZDKaeAkwA8QEPJ/CYfpBUCmqxp1E7uXJ6u0tUNVQbvIR+DIBzgHgQMvrZ5LZWIiSuowh6N+nQ9ZYgnNcLTzdRBsz5Ot1socWCr1KipYulrJVDIQjqjwgqsESvcPQB5QWmP2nsWem5X80IeizhaadPfHQwTxnXJTDk5ZQgeOOCC0ScY0wtPdRrc4AzY7BVZuQ8bVDhcXJLyhNnwJUFj5hTQVhmH8mQ7H5nYYkRxJw8hqBWYsLIr+gisKYobdjKguClOKLiQvgrrwqogiIr1wECHdRCCjIopNiCKZIGkthysrrWSklg36M6O5fT3fzmk7C8kDL4zt2/neP8/ze01/b6XuceWsVmAJJR56gurObjSn0/DWrEY152SmIyFBNk1sxZhBZAQTyVmEDjeiy9eAQdm4FK1gLlHg8Y3CYlXzZW12A1+GYd1Yi1u1LogXQSYgmxdnjM8jsTFNZvLMxADE3h0QS8vxNNqLJWKa2ZMXBRXwaaNmL/XdoUct+hhtjF+MFBZ+zJq5Gw6ywoRyI/xs/JjJtCj38+XWo3rGdM6pI3nlqYshmmiGx7fJpLE8rAqGZQy+49o5CNeauoeplJZZPbWfztqSWurvmV/ixrCXQjRSFQE/xI8R/dK44VJae99OorWt/Xh2tZxp0Q+903zynX/q6YLwevgy28IXyiBYxCY3cXYeIvGGRL0Isc697Z5k0NRMQj8Grd92zuDALuAuIRm8CVSohe1uYZ+T74FwADjdBFBh6GgH+mm3ZuLDSb9Ocg9YLNYZOeSyUitevupFeWWVTlzjQ0dNfQJW1fOybulPWlmyZ85wpshQC43/m+9YsR2ZTn9wg5z955+z2FO3H0PRIIqcHHzgPpAgNukwOJzAwCB3NiFQxsyyjJ37J4lMXklpfnaRyidaO3xe7+6h3JmVy2CjkcInD3EO3/T1xsFn3mobX0z+RzkfNAxcpXrIjo+RkFKp0VLkk1hfwwqJr69RODxbcH05gem/wIEN62TV13XuMxNIvqYYqCT6R6x14UHsEarkwhBxJbtnC4wmS9fXDuT2stFkxIDsp4tfbZU5MCr0jnMbIMLoKy7Gc8WunU3rxHMoCmKxUaiqij/5alOWhMPoGAAAAABJRU5ErkJggg==";
        const size = new OpenLayers.Size(20, 20);
        const icon = new OpenLayers.Icon(camIcon, size);
        const epsg4326 = new OpenLayers.Projection("EPSG:4326"); // WGS 1984 projection. Malaysia uses EPSG:900913
        const projectTo = W.map.getProjectionObject();
        const lonLat = new OpenLayers.LonLat(spec.lon, spec.lat).transform(epsg4326, projectTo);
        const newMarker = new OpenLayers.Marker(lonLat, icon);
        newMarker.idx = spec.idx;
        newMarker.title = spec.desc;
        newMarker.url = spec.src;
        newMarker.width = spec.width;
        newMarker.height = spec.height;
        newMarker.location = lonLat;
        newMarker.events.register("click", newMarker, this.popupCam);
        this.trafcamLayer.addMarker(newMarker);
    }
    popupCam(e) {
        popupCam_close(); // Close existing popup if already opened.
        var popupHTML = `<div id="gmPopupContainerCam" style="margin:1;text-align:center;padding:5px;z-index:1100;position:absolute;color:white;background:rgba(0,0,0,0.5)">
            <table border=0>
                <tr>
                    <td><div id="mycamdivheader" style="min-height:20px;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;width:380px">${e.object.title}</div></td>
                    <td align="right"><a href="#close" id="gmCloseCamDlgBtn" title="Close" style="color:red">X</a></td>
                </tr>
                <tr><td colspan=2>Select source:
                    <select id="wazemy_camSource">
                    </select>
                    <div hidden id="mycamid">${e.object.idx}</div>
                </td></tr>
                <tr><td colspan=2><img style="width:400px" id="staticimage"></td></tr>
                <tr><td colspan=2><div id="mycamstatus"></div></td></tr>
            </table></div>`;
        document.body.insertAdjacentHTML("afterbegin", popupHTML);
        // Handle cases where popup is too near the edge.
        let tw = $("#gmPopupContainerCam").width();
        let th = $("#gmPopupContainerCam").height() + 200;
        var tooltipX = e.clientX + window.scrollX + 15;
        var tooltipY = e.clientY + window.scrollY + 15;
        if (tooltipX + tw > W.map.$map.innerWidth()) {
            tooltipX -= tw + 20; // 20 = scroll bar size
            if (tooltipX < 0)
                tooltipX = 0;
        }
        if (tooltipY + th > W.map.$map.innerHeight()) {
            tooltipY -= th + 20;
            if (tooltipY < 0)
                tooltipY = 0;
        }
        $("#gmPopupContainerCam").css({ left: tooltipX });
        $("#gmPopupContainerCam").css({ top: tooltipY });
        //Add listener for popup's "Close" button
        const closeBtn = document.getElementById("gmCloseCamDlgBtn");
        closeBtn.onclick = popupCam_close;
        // Allow popup to be draggable.
        const popupContainerEl = document.getElementById("gmPopupContainerCam");
        popup_dragElement(popupContainerEl);
        const camSourceEl = document.getElementById("wazemy_camSource");
        for (let urlsrc in e.object.url) {
            if (urlsrc === "LLM" && e.object.url["LLM"].split("|").length == 2) {
                popup_appendOption(urlsrc);
            }
            else if (urlsrc === "Jalanow") {
                popup_appendOption(urlsrc);
            }
        }
        camSourceEl.onchange = (e) => {
            console.log("PluginTrafficCameras: Camera source selection changed.");
            const camId = document.getElementById("mycamid");
            const target = e.target;
            switch (target.selectedOptions[0].innerText) {
                case "Jalanow":
                    popup_getJalanowImage(trafficCamsData[camId.innerText]["url"]["Jalanow"]);
                    break;
                case "LLM":
                    popup_getLLMImage(trafficCamsData[camId.innerText]["url"]["LLM"]);
                    break;
            }
        };
        // Get image for the first time when popup is displayed.
        switch (Object.keys(e.object.url)[0]) {
            case "Jalanow":
                popup_getJalanowImage(e.object.url["Jalanow"]);
                break;
            case "LLM":
                popup_getLLMImage(e.object.url["LLM"]);
                break;
        }
        function popupCam_close() {
            const popupContainerEl = document.getElementById("gmPopupContainerCam");
            if (popupContainerEl) {
                popupContainerEl.remove();
                popupContainerEl.hidden = true;
            }
        }
        function popup_dragElement(elmnt) {
            var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
            if (document.getElementById("mycamdivheader")) {
                // if present, the header is where you move the DIV from:
                document.getElementById("mycamdivheader").onmousedown = dragMouseDown;
            }
            else {
                // otherwise, move the DIV from anywhere inside the DIV:
                elmnt.onmousedown = dragMouseDown;
            }
            function dragMouseDown(e) {
                e.preventDefault();
                // get the mouse cursor position at startup:
                pos3 = e.clientX;
                pos4 = e.clientY;
                document.onmouseup = closeDragElement;
                // call a function whenever the cursor moves:
                document.onmousemove = elementDrag;
            }
            function elementDrag(e) {
                e.preventDefault();
                // calculate the new cursor position:
                pos1 = pos3 - e.clientX;
                pos2 = pos4 - e.clientY;
                pos3 = e.clientX;
                pos4 = e.clientY;
                // set the element's new position:
                const popupContainerEl = document.getElementById("gmPopupContainerCam");
                popupContainerEl.style.top = popupContainerEl.offsetTop - pos2 + "px";
                popupContainerEl.style.left = popupContainerEl.offsetLeft - pos1 + "px";
            }
            function closeDragElement() {
                // stop moving when mouse button is released:
                document.onmouseup = null;
                document.onmousemove = null;
            }
        }
        function popup_appendOption(urlsrc) {
            const option = document.createElement("option");
            option.value = urlsrc;
            option.text = urlsrc;
            camSourceEl.append(option);
        }
        function popup_getJalanowImage(url) {
            GM_xmlhttpRequest({
                method: "GET",
                responseType: "blob",
                headers: {
                    authority: "p4.fgies.com",
                    referer: "https://www.jalanow.com/",
                    accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                },
                url: url,
                onload: function (response) {
                    const staticImageEl = document.getElementById("staticimage");
                    staticImageEl.src = URL.createObjectURL(response.response);
                    document.getElementById("mycamstatus").innerHTML = "";
                },
                onerror: function (response) {
                    document.getElementById("mycamstatus").innerHTML =
                        "Error loading image.";
                },
                onprogress: function (response) {
                    document.getElementById("mycamstatus").innerHTML = "Loading image...";
                },
            });
        }
        function popup_getLLMImage(url) {
            let camImg = url.split("|");
            GM_xmlhttpRequest({
                method: "GET",
                responseType: "blob",
                url: camImg[0],
                onload: function (response) {
                    const re = new RegExp('src="data:image/png;base64, ([A-Za-z0-9/+=]*)" title="' +
                        camImg[1] +
                        '"');
                    const m = response.responseText.match(re);
                    const staticImageEl = document.getElementById("staticimage");
                    staticImageEl.src = "data:image/png;base64," + m[1];
                    document.getElementById("mycamstatus").innerHTML = "";
                },
                onerror: function (response) {
                    document.getElementById("mycamstatus").innerHTML =
                        "Error loading image.";
                },
                onprogress: function (response) {
                    document.getElementById("mycamstatus").innerHTML = "Loading image...";
                },
            });
        }
    }
}

;// CONCATENATED MODULE: ./src/plugins/PluginKVMR.ts


class PluginKVMR {
    constructor() {
        this.areas = [
            {
                name: "Area 1",
                geometry: "POLYGON ((101.296968 3.210365, 101.30376 3.118812, 101.327352 3.100621, 101.332475 3.083048, 101.3447571 3.0671528, 101.3475037 2.9992702, 101.4546204 2.9862417, 101.443634 3.2097608, 101.296968 3.210365))",
                color: "#ffffff",
            },
            {
                name: "Area 2a",
                geometry: "POLYGON ((101.443634 3.2097608, 101.4546204 2.9862417, 101.542797963266 2.999451622765, 101.546214771469 3.20939581928522, 101.443634 3.2097608))",
                color: "#ff0000",
            },
            {
                name: "Area 2b",
                geometry: "POLYGON ((101.546208402411 3.20940523257651, 101.545212203573 3.14955753765693, 101.640751782444 3.14826179285013, 101.6379547 3.2090753, 101.546208402411 3.20940523257651))",
                color: "#00ff00",
            },
            {
                name: "Area 2c",
                geometry: "POLYGON ((101.545226515908 3.14956187716133, 101.544049591624 3.0763653393285, 101.643784992874 3.08223941174546, 101.640750558754 3.14825692296985, 101.545226515908 3.14956187716133))",
                color: "#0000ff",
            },
            {
                name: "Area 2d",
                geometry: "POLYGON ((101.544050641663 3.07636531606873, 101.542795175242 2.99943779058738, 101.6468811 3.0150413, 101.643784373198 3.08223705403019, 101.544050641663 3.07636531606873))",
                color: "#ffff00",
            },
            {
                name: "Area 3",
                geometry: "POLYGON ((101.6379547 3.2090753, 101.6412146 3.145488, 101.7443848 3.1501147, 101.7309414 3.2090495, 101.6379547 3.2090753))",
                color: "#ff00ff",
            },
            {
                name: "Area 4",
                geometry: "POLYGON ((101.6412146 3.145488, 101.6439612 3.0796673, 101.7615509 3.0791519, 101.7443848 3.1501147, 101.6412146 3.145488))",
                color: "#00ffff",
            },
            {
                name: "Area 5",
                geometry: "POLYGON ((101.788224 3.210399, 101.7309414 3.2090495, 101.7615509 3.0791519, 101.6439612 3.0796673, 101.6468811 3.0150413, 101.8391418 3.0253266, 101.788224 3.210399))",
                color: "#40ff00",
            },
            {
                name: "Area 6",
                geometry: "POLYGON ((101.332475 3.083048, 101.262098 3.074553, 101.2335205 3.0815516, 101.208976 3.060844, 101.162394 2.989639, 101.223721 2.903504, 101.268598 2.871386, 101.284902 2.830662, 101.448138 2.72835, 101.4546204 2.9862417, 101.3475037 2.9992702, 101.3447571 3.0671528, 101.332475 3.083048))",
                color: "#ff4000",
            },
            {
                name: "Area 7",
                geometry: "POLYGON ((101.4546204 2.9862417, 101.448138 2.72835, 101.477474 2.766274, 101.559411 2.807463, 101.6578674 2.8223442, 101.6468811 3.0150413, 101.4546204 2.9862417))",
                color: "#33ff00",
            },
            {
                name: "Area 8",
                geometry: "POLYGON ((101.6578674 2.8223442, 101.725067 2.83344, 101.756459 2.866068, 101.882828 2.870563, 101.8391418 3.0253266, 101.6468811 3.0150413, 101.6578674 2.8223442))",
                color: "#ff0033",
            },
        ];
        this.initialize();
    }
    initialize() {
        // Add settings into view.
        const settingsHTML = `<div>
      <input type="checkbox" id="wazemySettings_kvmr_enable" style="margin-top:0px"/>
      <label for="wazemySettings_kvmr_enable">Klang Valley Map Raid</label><br></div>`;
        $("#wazemySettings_settings").append(settingsHTML);
        const settingsEl = document.getElementById(`wazemySettings_kvmr_enable`);
        const savedSettings = SettingsStorage.instance.getSetting("kvmr");
        if (savedSettings?.enable === true) {
            settingsEl.checked = true;
        }
        else {
            settingsEl.checked = false;
        }
        settingsEl.onchange = (e) => {
            const target = e.target;
            PluginManager.instance.updatePluginSettings("kvmr", {
                enable: target.checked,
            });
        };
        // Add MR polygon overlay.
        const mro_Map = W.map;
        const mro_OL = OpenLayers;
        // const mro_mapLayers = mro_Map.getLayersBy("uniqueName", "__KlangValley");
        this.raid_mapLayer = new mro_OL.Layer.Vector("KlangValley", {
            displayInLayerSwitcher: true,
            uniqueName: "__KlangValley",
        });
        mro_Map.addLayer(this.raid_mapLayer);
        this.areas.forEach((area) => {
            const geometry = parseWKT(area.geometry);
            this.addRaidPolygon(this.raid_mapLayer, geometry, area.color, area.name);
        });
        mro_Map.events.register("moveend", W.map, function () {
            currentRaidLocation();
        });
        mro_Map.events.register("zoomend", W.map, function () {
            currentRaidLocation();
        });
        console.log("PluginKVMR initialized.");
        /**
         * Updates the current raid location on the map based on the user's current location.
         *
         * @return {void} This function does not return anything.
         */
        function currentRaidLocation() {
            // Only run if the plugin is enabled. Workaround because unregistering events doesn't work.
            if ($("#wazemySettings_kvmr_enable").is(":checked") === false) {
                return;
            }
            var mro_Map = W.map;
            const mro_mapLayers = mro_Map.getLayersBy("uniqueName", "__KlangValley")[0];
            for (let i = 0; i < mro_mapLayers.features?.length; i++) {
                var raidMapCenter = mro_Map.getCenter();
                var raidCenterPoint = new OpenLayers.Geometry.Point(raidMapCenter.lon, raidMapCenter.lat);
                const raid_mapLayer = mro_Map.getLayersBy("uniqueName", "__KlangValley")[0];
                var raidCenterCheck = raid_mapLayer.features[i].geometry.components[0].containsPoint(raidCenterPoint);
                var holes = raid_mapLayer.features[i].attributes.holes;
                if (raidCenterCheck === true) {
                    var str = $("#topbar-container > div > div.location-info-region > div").text();
                    const location = str.split(" - ");
                    if (location.length > 1) {
                        location[1] =
                            "Klang Valley MapRaid " +
                                raid_mapLayer.features[i].attributes.number;
                    }
                    else {
                        location.push("Klang Valley MapRaid " +
                            raid_mapLayer.features[i].attributes.number);
                    }
                    const raidLocationLabel = location.join(" - ");
                    setTimeout(function () {
                        $("#topbar-container > div > div.location-info-region > div").text(raidLocationLabel);
                    }, 200);
                    if (holes === "false") {
                        break;
                    }
                }
            }
        }
        function parseWKT(wkt) {
            let trimmed;
            if (wkt.startsWith("POLYGON")) {
                trimmed = wkt.replace("POLYGON ((", "").replace("))", "");
            }
            const coordinatePairs = trimmed.split(", ");
            const coordinates = coordinatePairs.map((pair) => {
                const [lon, lat] = pair.split(" ");
                return { lon, lat };
            });
            return coordinates;
        }
    }
    enable() {
        this.raid_mapLayer.setVisibility(true);
        console.log("PluginKVMR enabled.");
    }
    disable() {
        this.raid_mapLayer.setVisibility(false);
        const mro_map = W.map;
        mro_map.events.unregister("moveend", W.map);
        mro_map.events.unregister("zoomend", W.map);
        console.log("PluginKVMR disabled.");
    }
    updateSettings(settings) {
        if (settings.enable === true) {
            this.enable();
        }
        else {
            this.disable();
        }
        console.log("PluginKVMR settings updated", settings);
    }
    addRaidPolygon(raidLayer, groupPoints, groupColor, groupNumber) {
        var mro_Map = W.map;
        var mro_OL = OpenLayers;
        var raidGroupLabel = "KlangValley " + groupNumber;
        var groupName = "RaidGroup " + groupNumber;
        var style = {
            strokeColor: groupColor,
            strokeOpacity: 0.8,
            strokeWidth: 3,
            fillColor: groupColor,
            fillOpacity: 0.15,
            label: raidGroupLabel,
            labelOutlineColor: "black",
            labelOutlineWidth: 3,
            fontSize: 14,
            fontColor: groupColor,
            fontOpacity: 0.85,
            fontWeight: "bold",
        };
        var attributes = {
            name: groupName,
            number: groupNumber,
        };
        var pnt = [];
        for (let i = 0; i < groupPoints.length; i++) {
            const convPoint = new OpenLayers.Geometry.Point(groupPoints[i].lon, groupPoints[i].lat).transform(new OpenLayers.Projection("EPSG:4326"), mro_Map.getProjectionObject());
            //console.log('MapRaid: ' + JSON.stringify(groupPoints[i]) + ', ' + groupPoints[i].lon + ', ' + groupPoints[i].lat);
            pnt.push(convPoint);
        }
        var ring = new mro_OL.Geometry.LinearRing(pnt);
        var polygon = new mro_OL.Geometry.Polygon([ring]);
        var feature = new mro_OL.Feature.Vector(polygon, attributes, style);
        raidLayer.addFeatures([feature]);
    }
}

;// CONCATENATED MODULE: ./src/plugins/PluginZoomPic.ts
class PluginZoomPic {
    constructor() {
        this.initialize();
    }
    /**
     * Initialize plugin.
     *
     * @return {void} This function does not return anything.
     */
    initialize() {
        $(document.body).on("click", () => {
            const img = $(".venue-image-dialog > wz-dialog-content > img");
            if (img.length > 0) {
                const newImg = img[0];
                const links = $(".venue-image-dialog > wz-dialog-header > a");
                for (let i = 0; i < links.length; i++) {
                    links[i].remove();
                }
                const newImgHTML = `<a href="${newImg.src.replace("thumbs/thumb700_", "")}" target="_blank">(+)</a>`;
                $("wz-dialog-header").append(newImgHTML);
            }
            // $("div.modal-dialog.venue-image-dialog");
        });
        console.log("[WazeMY] PluginZoomPic initialized.");
    }
    /**
     * Enable plugin.
     *
     * @return {void} This function does not return anything.
     */
    enable() {
        console.log("[WazeMY] PluginZoomPic enabled.");
    }
    /**
     * Disable plugin.
     *
     * @return {void} This function does not return anything.
     */
    disable() {
        console.log("[WazeMY] PluginZoomPic disabled.");
    }
    /**
     * Updates the settings of the PluginZoomPic based on the provided settings object.
     *
     * @return {void} This function does not return anything.
     */
    updateSettings(settings) {
        console.log("[WazeMY] PluginZoomPic settings updated.", settings);
    }
}

;// CONCATENATED MODULE: ./src/plugins/PluginPlaces.ts


class PluginPlaces {
    constructor() {
        this.tabHTML = `
    <div><h4>WazeMY Places</h4></div>
    <div id="wazemyPlaces">
      <select name="wazemyPlaces_polygons" id="wazemyPlaces_polygons"></select>
      <button id="wazemyPlaces_scan">Scan</button>
      <div id="wazemyPlaces_scanStatus"></div>
      <div id="wazemyPlaces_purCount"></div>
      <div id="wazemyPlaces_totalCount"></div>
      <div id="wazemyPlaces_table">
      <table id="wazemyPlaces_venues">
        <thead>
          <tr>
            <th title="I=Image\nN=New Place\nU=Update\nF=Flag\nD=Delete">PUR</th>
            <th>L</th>
            <th>Name</th>
            <th>Errors</th>
          </tr>
        </thead>
        <tbody></tbody>
      </table>
      </div>
    </div>
  `;
        this.initialize();
    }
    /**
     * Initialize plugin.
     *
     * @return {void} This function does not return anything.
     */
    initialize() {
        const settingsHTML = `<input type="checkbox" id="wazemySettings_places_enable"/>
      <label for="wazemySettings_places_enable">Enable Places</label>`;
        $("#wazemySettings_settings").append(settingsHTML);
        $("#wazemySettings_places_enable").on("change", () => {
            PluginManager.instance.updatePluginSettings("places", {
                enable: $("#wazemySettings_places_enable").prop("checked"),
            });
        });
        // Set settings according to last stored value.
        const savedSettings = SettingsStorage.instance.getSetting("places");
        if (savedSettings?.enable === true) {
            $("#wazemySettings_places_enable").prop("checked", true);
        }
        else {
            $("#wazemySettings_places_enable").prop("checked", false);
        }
        console.log("[WazeMY] PluginPlaces initialized.");
    }
    /**
     * Enable plugin.
     *
     * @return {void} This function does not return anything.
     */
    enable() {
        const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("wazemyplaces");
        tabLabel.innerHTML = "WazeMY Places";
        tabLabel.title = "WazeMY Places";
        tabPane.innerHTML = this.tabHTML;
        // Populate select options with polygons from KVMR.
        const map = W.map.getLayersBy("uniqueName", "__KlangValley");
        map[0].features.forEach((feature) => {
            $("#wazemyPlaces_polygons").append($("<option>", {
                value: feature.data.number,
                text: feature.data.number,
            }));
        });
        // Handle Scan button.
        $("#wazemyPlaces_scan").on("click", async () => {
            $("#wazemyPlaces_scanStatus").text("Scanning tiles.");
            $("#wazemyPlaces_venues > tbody").empty();
            const map = W.map.getLayersBy("uniqueName", "__KlangValley");
            if (map.length === 0) {
                console.log("[PluginPlaces] No KVMR layer found. Aborting scan.");
                return false;
            }
            const mr = map[0].getFeaturesByAttribute("number", $("#wazemyPlaces_polygons option:selected")[0].innerText);
            if (mr.length === 0) {
                console.log("[PluginPlaces] No polygon found. Aborting scan.");
                return false;
            }
            const feature = mr[0];
            let bounds = feature.geometry.getBounds().clone();
            bounds = bounds.transform(W.map.getProjectionObject(), "EPSG:4326");
            const venues = await getAllVenues(bounds);
            let purCount = 0;
            let totalCount = 0;
            venues.forEach((venue) => {
                // Check venue against rules.
                const status = evaluateVenue(venue);
                const isPUR = checkPURstatus(venue);
                if (status.priority > 0 || isPUR) {
                    // Add venue to table.
                    let lon = 0;
                    let lat = 0;
                    if (venue.geometry.type === "Polygon") {
                        lon = venue.geometry.coordinates[0][0][0];
                        lat = venue.geometry.coordinates[0][0][1];
                    }
                    else {
                        lon = venue.geometry.coordinates[0];
                        lat = venue.geometry.coordinates[1];
                    }
                    const row = $("<tr>");
                    row.attr("id", `${lon}:${lat}:${venue.id}`);
                    row.on("click", (e) => {
                        const target = e.currentTarget.id.split(":"); // split to lon:lat:id
                        const xy = OpenLayers.Layer.SphericalMercator.forwardMercator(parseFloat(target[0]), parseFloat(target[1]));
                        W.map.setCenter(xy);
                    });
                    let purHTML = ``;
                    if (isPUR) {
                        purCount++;
                        if (venue.approved === false) {
                            purHTML = `<td align="center">N</td>`;
                        }
                        else if (venue.venueUpdateRequests[0].type === "REQUEST") {
                            if (venue.venueUpdateRequests[0].subType === "FLAG") {
                                purHTML = `<td align="center">F</td>`;
                            }
                            else if (venue.venueUpdateRequests[0].subType === "UPDATE") {
                                purHTML = `<td align="center">U</td>`;
                            }
                            else if (venue.venueUpdateRequests[0].subType === "DELETE") {
                                purHTML = `<td align="center">D</td>`;
                            }
                            else {
                                purHTML = `<td align="center">+</td>`;
                            }
                        }
                        else if (venue.venueUpdateRequests[0].type === "IMAGE") {
                            purHTML = `<td align="center">I</td>`;
                        }
                        else {
                            purHTML = `<td align="center">+</td>`;
                        }
                    }
                    else {
                        purHTML = `<td></td>`;
                    }
                    row.append(purHTML);
                    const levelHTML = `<td>${venue.lockRank ? venue.lockRank + 1 : 1}</td>`;
                    row.append(levelHTML);
                    const colHTML = `<td>${venue.name}</td>`;
                    row.append(colHTML);
                    const errorsHTML = `<td>${status.errors.join("\r\n")}</td>`;
                    row.append(errorsHTML);
                    $("#wazemyPlaces_venues > tbody").append(row);
                    totalCount++;
                }
                $("#wazemyPlaces_purCount").text(`# PUR = ${purCount}`);
                $("#wazemyPlaces_totalCount").text(`# total = ${totalCount}`);
                $("#wazemyPlaces_scanStatus").text("");
                function evaluateVenue(venue) {
                    let status = {
                        priority: 0,
                        errors: [],
                    };
                    // Rule #1
                    if (typeof venue.name == "undefined") {
                        if (!venue.categories.includes("RESIDENCE_HOME")) {
                            status.priority = 3;
                            status.errors.push("Missing name.");
                        }
                    }
                    else {
                        // Rule: Check name for all uppercase.
                        if (venue.name === venue.name.toUpperCase()) {
                            status.priority = 3;
                            status.errors.push("Name is uppercase.");
                        }
                        // Rule: Check name for all lowercase.
                        if (venue.name === venue.name.toLowerCase()) {
                            status.priority = 3;
                            status.errors.push("Name is lowercase.");
                        }
                    }
                    // Rule: Min lock is not set.
                    if (venue.lockRank === 0) {
                        status.priority = 3;
                        status.errors.push("Min lock not set.");
                    }
                    // Rule: Phone number format.
                    if (venue.phone) {
                        if (/^[\d]{3}-[\d]{3} [\d]{4}$/.test(venue.phone) === false &&
                            /^[\d]{3}-[\d]{4} [\d]{4}$/.test(venue.phone) === false &&
                            /^[\d]{2}-[\d]{4} [\d]{4}$/.test(venue.phone) === false &&
                            /^[\d]{2}-[\d]{3} [\d]{4}$/.test(venue.phone) === false &&
                            /^[\d]{3}-[\d]{3} [\d]{3}$/.test(venue.phone) === false &&
                            /^[\d]{1}-[\d]{3}-[\d]{2}-[\d]{4}$/.test(venue.phone) === false) {
                            status.priority = 2;
                            status.errors.push("Phone number format incorrect.");
                        }
                    }
                    // Rule: Category specific rank locks.
                    if ((venue.categories.includes("CHARGING_STATION") &&
                        venue.lockRank < 3) ||
                        (venue.categories.includes("GAS_STATION") && venue.lockRank < 3) ||
                        (venue.categories.includes("AIRPORT") && venue.lockRank < 4) ||
                        (venue.categories.includes("BUS_STATION") && venue.lockRank < 2) ||
                        (venue.categories.includes("FERRY_PIER") && venue.lockRank < 2) ||
                        (venue.categories.includes("JUNCTION_INTERCHANGE") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("REST_AREAS") && venue.lockRank < 2) ||
                        (venue.categories.includes("SEAPORT_MARINA_HARBOR") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("TRAIN_STATION") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("TUNNEL") && venue.lockRank < 2) ||
                        (venue.categories.includes("CITY_HALL") && venue.lockRank < 2) ||
                        (venue.categories.includes("COLLEGE_UNIVERSITY") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("COURTHOUSE") && venue.lockRank < 2) ||
                        (venue.categories.includes("DOCTOR_CLINIC") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("EMBASSY_CONSULATE") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("FIRE_DEPARTMENT") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("HOSPITAL_URGENT_CARE") &&
                            venue.lockRank < 3) ||
                        (venue.categories.includes("LIBRARY") && venue.lockRank < 2) ||
                        (venue.categories.includes("MILITARY") && venue.lockRank < 3) ||
                        (venue.categories.includes("POLICE_STATION") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("PRISON_CORRECTIONAL_FACILITY") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("RELIGIOUS_CENTER") &&
                            venue.lockRank < 3) ||
                        (venue.categories.includes("SCHOOL") && venue.lockRank < 2) ||
                        (venue.categories.includes("BANK_FINANCIAL") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("SHOPPING_CENTER") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("MUSEUM") && venue.lockRank < 2) ||
                        (venue.categories.includes("RACING_TRACK") && venue.lockRank < 2) ||
                        (venue.categories.includes("STADIUM_ARENA") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("THEME_PARK") && venue.lockRank < 2) ||
                        (venue.categories.includes("TOURIST_ATTRACTION_HISTORIC_SITE") &&
                            venue.lockRank < 2) ||
                        (venue.categories.includes("ZOO_AQUARIUM") && venue.lockRank < 2) ||
                        (venue.categories.includes("BEACH") && venue.lockRank < 2) ||
                        (venue.categories.includes("GOLF_COURSE") && venue.lockRank < 2) ||
                        (venue.categories.includes("PARK") && venue.lockRank < 2) ||
                        (venue.categories.includes("FOREST_GROVE") && venue.lockRank < 2) ||
                        (venue.categories.includes("ISLAND") && venue.lockRank < 4) ||
                        (venue.categories.includes("RIVER_STREAM") && venue.lockRank < 3) ||
                        (venue.categories.includes("SEA_LAKE_POOL") &&
                            venue.lockRank < 5) ||
                        (venue.categories.includes("CANAL") && venue.lockRank < 2) ||
                        (venue.categories.includes("SWAMP_MARSH") && venue.lockRank < 2)) {
                        status.priority = 2;
                        status.errors.push("Min lock incorrect.");
                    }
                    return status;
                }
                function checkPURstatus(venue) {
                    if (venue.venueUpdateRequests?.length > 0) {
                        return true;
                    }
                    else {
                        return false;
                    }
                }
            });
            async function getAllVenues(bounds) {
                let venues = [];
                // console.log(bounds);
                const baseURL = "https://www.waze.com/row-Descartes/app/Features?language=en&v=2&cameras=true&mapComments=true&roadClosures=true&roadTypes=1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C15%2C16%2C17%2C18%2C19%2C20%2C22&venueLevel=4&venueFilter=1%2C1%2C1%2C1&";
                let urls = [];
                const stepSize = 0.1;
                for (let left = bounds.left; left <= bounds.right; left += stepSize) {
                    for (let bottom = bounds.bottom; bottom <= bounds.top; bottom += stepSize) {
                        urls.push(`bbox=${left}%2C${bottom}%2C${left + stepSize > bounds.right ? bounds.right : left + stepSize}%2C${bottom + stepSize > bounds.top ? bounds.top : bottom + stepSize}`);
                    }
                }
                for (let i = 0; i < urls.length; i++) {
                    // console.log(baseURL + urls[i]);
                    $("#wazemyPlaces_scanStatus").text(`Scanning tile ${i + 1} of ${urls.length}.`);
                    const result = await GM.xmlHttpRequest({
                        method: "GET",
                        responseType: "json",
                        url: baseURL + urls[i],
                    }).catch((e) => console.error(e));
                    venues = venues.concat(result.response.venues.objects);
                }
                return venues;
            }
        });
        console.log("[WazeMY] PluginPlaces enabled.");
    }
    /**
     * Disable plugin.
     *
     * @return {void} This function does not return anything.
     */
    disable() {
        if ($("span[title='WazeMY Places']").length > 0) {
            W.userscripts.removeSidebarTab("wazemyplaces");
        }
        console.log("[WazeMY] PluginPlaces disabled.");
    }
    /**
     * Updates the settings of the PluginPlaces based on the provided settings object.
     *
     * @return {void} This function does not return anything.
     */
    updateSettings(settings) {
        if (settings.enable === true) {
            this.enable();
        }
        else {
            this.disable();
        }
        console.log("[WazeMY] PluginPlaces settings updated.", settings);
    }
}

;// CONCATENATED MODULE: ./src/PluginFactory.ts






class PluginFactory {
    static createPlugin(pluginName) {
        switch (pluginName) {
            case "PluginTooltip":
                return new PluginTooltip();
            case "PluginCopyLatLon":
                return new PluginCopyLatLon();
            case "PluginTrafficCameras":
                return new PluginTrafficCameras();
            case "PluginKVMR":
                return new PluginKVMR();
            case "PluginZoomPic":
                return new PluginZoomPic();
            case "PluginPlaces":
                return new PluginPlaces();
            default:
                throw new Error(`Unknown plugin: ${pluginName}`);
        }
    }
}

;// CONCATENATED MODULE: ./src/PluginManager.ts


class PluginManager {
    constructor(settings) {
        this.plugins = {};
        this.settingsStorage = settings;
    }
    /**
     * Adds a plugin to the PluginManager.
     *
     * @param {string} key - The key to associate the plugin with.
     * @param {string} type - The type of plugin to create.
     * @return {void} This function does not return anything.
     */
    addPlugin(key, type) {
        const plugin = PluginFactory.createPlugin(type);
        this.plugins[key] = plugin;
        const pluginSettings = this.settingsStorage.getSetting(key);
        if (pluginSettings) {
            plugin.updateSettings(pluginSettings);
        }
    }
    /**
     * Removes a plugin from the PluginManager.
     *
     * @param {string} key - The key associated with the plugin to remove.
     * @return {void} This function does not return anything.
     */
    removePlugin(key) {
        if (this.plugins[key]) {
            this.settingsStorage.removeSetting(key);
            delete this.plugins[key];
        }
    }
    /**
     * Enables a plugin with the given key if it exists.
     *
     * @param {string} key - The key of the plugin to enable.
     * @return {void} This function does not return anything.
     */
    enablePlugin(key) {
        if (this.plugins[key]) {
            this.plugins[key].enable();
        }
    }
    /**
     * Disables a plugin with the given key if it exists.
     *
     * @param {string} key - The key of the plugin to disable.
     * @return {void} This function does not return anything.
     */
    disablePlugin(key) {
        if (this.plugins[key]) {
            this.plugins[key].disable();
        }
    }
    /**
     * Updates the settings of a plugin associated with the given key.
     *
     * @param {string} key - The key associated with the plugin.
     * @param {any} settings - The new settings to be applied to the plugin.
     * @return {void} This function does not return anything.
     */
    updatePluginSettings(key, settings) {
        if (this.plugins[key]) {
            this.plugins[key].updateSettings(settings);
            this.settingsStorage.updateSetting(key, settings);
        }
    }
}
PluginManager.instance = new PluginManager(SettingsStorage.instance);

;// CONCATENATED MODULE: ./src/index.ts


const updateMessage = `PluginTooltip: Display POI categories.`;
async function src_main() {
    console.log("[WazeMY] Script started");
    document.addEventListener("wme-ready", initializeWazeMY, { once: true });
}
async function initializeWazeMY() {
    console.log("[WazeMY] WME ready");
    const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("wazemy");
    tabLabel.innerHTML = "WazeMY";
    tabLabel.title = "WazeMY";
    tabPane.innerHTML = `<div>
  <h4>WazeMY</h4>
  <b>${GM_info.script.version}</b>
</div>
<fieldset class="wazemySettings">
  <legend class="wazemySettingsLegend">
    <h6>Settings</h6></legend>
  <div id="wazemySettings_settings"></div>
</fieldset>
<fieldset class="wazemySettings">
  <legend class="wazemySettingsLegend">
  <h6>Shortcuts</h6></legend>
  <div id="wazemySettings_shortcuts">
  </div>
</fieldset>`;
    WazeWrap.Interface.ShowScriptUpdate("WME WazeMY", GM_info.script.version, updateMessage, "https://greasyfork.org/en/scripts/404584-wazemy", "javascript:alert('No forum available');");
    const pluginManager = PluginManager.instance;
    pluginManager.addPlugin("copylatlon", "PluginCopyLatLon");
    pluginManager.addPlugin("tooltip", "PluginTooltip");
    pluginManager.addPlugin("trafcam", "PluginTrafficCameras");
    pluginManager.addPlugin("kvmr", "PluginKVMR");
    pluginManager.addPlugin("zoompic", "PluginZoomPic");
    pluginManager.addPlugin("places", "PluginPlaces");
}
src_main().catch((e) => {
    console.log("WazeMY: Bootstrap");
    console.log(e);
});

})();

/******/ })()
;