您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a city overlay for selected states
// ==UserScript== // @name WME Cities Overlay // @namespace https://gf.qytechs.cn/en/users/166843-wazedev // @version 2025.07.02.00 // @description Adds a city overlay for selected states // @author WazeDev // @match https://www.waze.com/*/editor* // @match https://www.waze.com/editor* // @match https://beta.waze.com/* // @exclude https://www.waze.com/*user/*editor/* // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js // @require https://gf.qytechs.cn/scripts/369729-wme-cities-overlay-db/code/WME%20Cities%20Overlay%20DB.js // @require https://update.gf.qytechs.cn/scripts/524747/1542062/GeoKMLer.js // @license GNU GPLv3 // @grant GM_xmlhttpRequest // @connect api.github.com // @connect raw.githubusercontent.com // @contributionURL https://github.com/WazeDev/Thank-The-Authors // ==/UserScript== /* ecmaVersion 2017 */ /* global $ */ /* global idbKeyval */ /* global WazeWrap */ /* global I18n */ /* eslint curly: ["warn", "multi-or-nest"] */ (function () { 'use strict'; const debug = false; const scriptMetadata = GM_info.script; const scriptName = scriptMetadata.name; const repoOwner = scriptMetadata.author; // Change this to a different repo username when testing a forked branch! const _settingsStoreName = '_wme_cities'; let _settings; let _kml; // Holds the raw input KML File data let _layer = null; // Holds the geoJSON converted features to map with the SDK const layerid = scriptName.replace(/[^a-z0-9_-]/gi, '_'); //Default settings const _color = '#E6E6E6'; const defaultFillOpacity = 0.3; const defaultStrokeOpacity = 0.6; const noFillStrokeOpacity = 0.9; let currState = ''; let currCity = []; let kmlCache = {}; loadSettings(); const _US_States = { Alabama: 'AL', Alaska: 'AK', Arizona: 'AZ', Arkansas: 'AR', California: 'CA', Colorado: 'CO', Connecticut: 'CT', 'District of Columbia': 'DC', Delaware: 'DE', Florida: 'FL', Georgia: 'GA', Hawaii: 'HI', Idaho: 'ID', Illinois: 'IL', Indiana: 'IN', Iowa: 'IA', Kansas: 'KS', Kentucky: 'KY', Louisiana: 'LA', Maine: 'ME', Maryland: 'MD', Massachusetts: 'MA', Michigan: 'MI', Minnesota: 'MN', Mississippi: 'MS', Missouri: 'MO', Montana: 'MT', Nebraska: 'NE', Nevada: 'NV', 'New Hampshire': 'NH', 'New Jersey': 'NJ', 'New Mexico': 'NM', 'New York': 'NY', 'North Carolina': 'NC', 'North Dakota': 'ND', Ohio: 'OH', Oklahoma: 'OK', Oregon: 'OR', Pennsylvania: 'PA', 'Rhode Island': 'RI', 'South Carolina': 'SC', 'South Dakota': 'SD', Tennessee: 'TN', Texas: 'TX', Utah: 'UT', Vermont: 'VT', Virginia: 'VA', Washington: 'WA', 'West Virginia': 'WV', Wisconsin: 'WI', Wyoming: 'WY', getAbbreviation: function (state) { return this[state]; }, getStateFromAbbr: function (abbr) { return Object.entries(_US_States).filter((x) => { if (x[1] == abbr) return x; })[0][0]; }, getStatesArray: function () { return Object.keys(_US_States).filter((x) => { if (typeof _US_States[x] !== 'function') return x; }); }, getStateAbbrArray: function () { return Object.values(_US_States).filter((x) => { if (typeof x !== 'function') return x; }); }, }; const _MX_States = { Aguascalientes: 'AGS', 'Baja California': 'BC', 'Baja California Sur': 'BCS', Campeche: 'CAM', 'Coahuila de Zaragoza': 'COAH', Colima: 'COL', Chiapas: 'CHIS', Durango: 'DGO', 'Ciudad de México': 'CDMX', Guanajuato: 'GTO', Guerrero: 'GRO', Hidalgo: 'HGO', Jalisco: 'JAL', 'Estado de México': 'EM', 'Michoacán de Ocampo': 'MICH', Morelos: 'MOR', Nayarit: 'NAY', 'Nuevo León': 'NL', Oaxaca: 'OAX', Puebla: 'PUE', 'Quintana Roo': 'QROO', Querétaro: 'QRO', 'San Luis Potosí': 'SLP', Sinaloa: 'SIN', Sonora: 'SON', Tabasco: 'TAB', Tamaulipas: 'TAM', Tlaxcala: 'TLAX', 'Veracruz Ignacio de la Llave': 'VER', Yucatán: 'YUC', Zacatecas: 'ZAC', getAbbreviation: function (state) { return this[state]; }, getStateFromAbbr: function (abbr) { return Object.entries(_MX_States).filter((x) => { if (x[1] == abbr) return x; })[0][0]; }, getStatesArray: function () { return Object.keys(_MX_States).filter((x) => { if (typeof _MX_States[x] !== 'function') return x; }); }, getStateAbbrArray: function () { return Object.values(_MX_States).filter((x) => { if (typeof x !== 'function') return x; }); }, }; let wmeSDK; // Declare wmeSDK globally // Ensure SDK_INITIALIZED is available if (unsafeWindow.SDK_INITIALIZED) { unsafeWindow.SDK_INITIALIZED.then(bootstrap).catch((err) => { console.error(`${scriptName}: SDK initialization failed`, err); }); } else { console.warn(`${scriptName}: SDK_INITIALIZED is undefined`); } function bootstrap() { wmeSDK = unsafeWindow.getWmeSdk({ scriptId: scriptName.replaceAll(' ', ''), scriptName: scriptName, }); // Use Promise.all to check readiness of all dependencies Promise.all([isWmeReady(), isWazeWrapReady(), isGeoKMLerReady()]) .then(() => { console.log(`${scriptName}: All dependencies are ready.`); init(); console.log(`${scriptName}: Initialized`); }) .catch((error) => { console.error(`${scriptName}: Error during bootstrap -`, error); }); } function isWmeReady() { return new Promise((resolve, reject) => { if (wmeSDK && wmeSDK.State.isReady() && wmeSDK.Sidebar && wmeSDK.LayerSwitcher && wmeSDK.Shortcuts && wmeSDK.Events) { console.log(`${scriptName}: WME is already ready.`); resolve(); } else { wmeSDK.Events.once({ eventName: 'wme-ready' }) .then(() => { if (wmeSDK.Sidebar && wmeSDK.LayerSwitcher && wmeSDK.Shortcuts && wmeSDK.Events) { console.log(`${scriptName}: WME is fully ready now.`); resolve(); } else { reject(`${scriptName}: Some SDK components are not loaded.`); } }) .catch((error) => { console.error(`${scriptName}: Error while waiting for WME to be ready:`, error); reject(error); }); } }); } function isWazeWrapReady() { return new Promise((resolve, reject) => { const maxTries = 1000; const checkInterval = 500; (function check(tries = 0) { if (unsafeWindow.WazeWrap && unsafeWindow.WazeWrap.Ready) { console.log(`${scriptName}: WazeWrap is successfully loaded.`); resolve(); } else if (tries < maxTries) { setTimeout(() => check(++tries), checkInterval); } else { reject(`${scriptName}: WazeWrap took too long to load.`); } })(); }); } function isGeoKMLerReady() { return new Promise((resolve, reject) => { try { if (typeof GeoKMLer !== 'undefined') { const geoKMLer = new GeoKMLer(); if (geoKMLer) { console.log(`${scriptName}: GeoKMLer is successfully loaded and ready.`); resolve(); } else { reject(`${scriptName}: GeoKMLer instance could not be created.`); } } else { reject(`${scriptName}: GeoKMLer is not defined.`); } } catch (error) { console.error(`${scriptName}: Error during GeoKMLer readiness check:`, error); reject(error); } }); } function isChecked(checkboxId) { return $('#' + checkboxId).is(':checked'); } function setChecked(checkboxId, checked) { $('#' + checkboxId).prop('checked', checked); } function loadSettings() { _settings = $.parseJSON(localStorage.getItem(_settingsStoreName)); let _defaultsettings = { layerVisible: true, ShowCityLabels: true, FillPolygons: true, HighlightFocusedCity: true, AutoUpdateKMLs: true, }; if (!_settings) _settings = _defaultsettings; for (var prop in _defaultsettings) { if (!_settings.hasOwnProperty(prop)) _settings[prop] = _defaultsettings[prop]; } } function saveSettings() { if (localStorage) { var settings = { layerVisible: _settings.layerVisible, ShowCityLabels: _settings.ShowCityLabels, FillPolygons: _settings.FillPolygons, HighlightFocusedCity: _settings.HighlightFocusedCity, AutoUpdateKMLs: _settings.AutoUpdateKMLs, }; localStorage.setItem(_settingsStoreName, JSON.stringify(settings)); } } function stripElevation(coordinates) { if (Array.isArray(coordinates[0])) { // If coordinates are nested, recursively strip elevation return coordinates.map((coord) => stripElevation(coord)); } // Remove third element from a single set of coordinates return coordinates.slice(0, 2); } /** * Function: flattenGeoJSON * ------------------------ * Flattens a GeoJSON "FeatureCollection" into an array of individual GeoJSON features, ensuring consistent * type casing and performing property cleanup, including the removal of unwanted characters. * * Parameters: * @param {Object} geoJson - The GeoJSON object to be flattened, expected to be a FeatureCollection. * @returns {Array} - An array of individual GeoJSON features, each with cleaned properties and standardized types. * * Throws: * - {Error} Throws an error if the GeoJSON input is invalid by not being a FeatureCollection. * * Description: * - Processes a FeatureCollection by iterating over each feature using `geomEach` to handle various geometry types. * - Geometry types such as MultiPoint, MultiLineString, and MultiPolygon are decomposed into individual features. * - Cleans feature properties, stripping unwanted markers and creating a `labelText` from the `name` attribute. * - Supports these GeoJSON geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection. * * Internal Functions: * - `updateGeoJSONType`: Ensures consistent casing of GeoJSON types using a lookup map. * - `stripElevation`: Removes elevation data from coordinates for more straightforward processing. * * Example Usage: * ``` * const flattenedFeatures = flattenGeoJSON(myGeoJSON); * ``` */ function flattenGeoJSON(geoJson) { // Verify and extract features array from the input GeoJSON if (geoJson.type !== 'FeatureCollection' || !Array.isArray(geoJson.features)) { throw new Error('Invalid GeoJSON input: expected a FeatureCollection.'); } const features = geoJson.features; const geoJSONTypeMap = { FEATURECOLLECTION: 'FeatureCollection', FEATURE: 'Feature', GEOMETRYCOLLECTION: 'GeometryCollection', POINT: 'Point', LINESTRING: 'LineString', POLYGON: 'Polygon', MULTIPOINT: 'MultiPoint', MULTILINESTRING: 'MultiLineString', MULTIPOLYGON: 'MultiPolygon', }; const updateGeoJSONType = (type) => geoJSONTypeMap[type.toUpperCase()] || type; return features.flatMap((feature) => { const flattenedGeometries = []; if (feature.properties) { const nameKey = ['name', 'Name', 'NAME'].find((key) => feature.properties[key]); if (nameKey) { feature.properties[nameKey] = feature.properties[nameKey].replace(/<at><openparen>/gi, '').replace(/<closeparen>/gi, ''); feature.properties.labelText = feature.properties[nameKey]; } } geomEach(feature.geometry, (geometry) => { const type = geometry === null ? null : updateGeoJSONType(geometry.type); switch (type) { case 'Point': case 'LineString': case 'Polygon': flattenedGeometries.push({ type: updateGeoJSONType('Feature'), geometry: { type: type, coordinates: stripElevation(geometry.coordinates), }, properties: feature.properties, }); break; case 'MultiPoint': case 'MultiLineString': case 'MultiPolygon': const geomType = updateGeoJSONType(type.split('Multi')[1]); geometry.coordinates.forEach((coordinate) => { flattenedGeometries.push({ type: updateGeoJSONType('Feature'), geometry: { type: geomType, coordinates: stripElevation(coordinate), }, properties: feature.properties, }); }); break; case 'GeometryCollection': geometry.geometries.forEach((geom) => { const updatedType = updateGeoJSONType(geom.type); flattenedGeometries.push({ type: updateGeoJSONType('Feature'), geometry: { type: updatedType, coordinates: stripElevation(geom.coordinates), }, properties: feature.properties, }); }); break; default: throw new Error(`Unknown Geometry Type: ${type}`); } }); return flattenedGeometries; }); } /** * Function: geomEach * ------------------ * Iterates over each geometry within a feature, handling different types of geometries * and their coordinate structures, and executing a specified callback function. * * This function supports: * - Simple geometries: Point, LineString, Polygon * - Compound geometries: MultiPoint, MultiLineString, MultiPolygon * - Geometry collections: GeometryCollection * * Parameters: * @param {Object} geometry - The geometry object extracted from a feature, containing * type and coordinates or sub-geometries. * @param {Function} callback - A callback function to execute for each geometry type, * receiving a geometry object as an argument. * * Notes: * - For compound geometries, coordinates are processed individually, and elevation data * is stripped using `stripElevation`. * - Throws an error if an unknown geometry type is encountered. **/ function geomEach(geometry, callback) { const type = geometry === null ? null : geometry.type; switch (type) { case 'Point': case 'LineString': case 'Polygon': callback(geometry); break; case 'MultiPoint': case 'MultiLineString': case 'MultiPolygon': geometry.coordinates.forEach((coordinate) => { callback({ type: type.split('Multi')[1], coordinates: stripElevation(coordinate), // Use stripElevation here }); }); break; case 'GeometryCollection': geometry.geometries.forEach(callback); break; default: throw new Error(`Unknown Geometry Type: ${type}`); } } function GetFeaturesFromKMLString(strKML) { const geoKMLer = new GeoKMLer(); const kmlDoc = geoKMLer.read(strKML); const GeoJSONflat = flattenGeoJSON(geoKMLer.toGeoJSON(kmlDoc, false)); // false = don't need the added CRS info section return GeoJSONflat; } /** * Function: findCurrCity * ---------------------- * Determines the current city based on the map's center point, identifying its feature * within GeoJSON layers, and handling DOM element retrieval for the current feature. * * Steps: * 1. Initialize the `cityData` object with default properties. * 2. Retrieve the current map center coordinates using `wmeSDK.Map`. * 3. Iterate over all features in the global `_layer` array to check if the map center is * within any polygon feature using `isPointInPolygon`. * - If a match is found, update `cityData` with the feature's details. * 4. Perform a debug-only operation to find the DOM element associated with the feature: * - Retrieve using `wmeSDK.Map.getFeatureDomElement` if the `featureId` is valid. * - Handle cases where the DOM element is not found or retrieval errors occur. * 5. Log the finalized `cityData` object for debugging purposes. * * Globals: * - `scriptName`: Used for logging errors and debug information. * - `_layer`: Array of GeoJSON features representing map polygons and properties. * - `debug`: Flag to enable additional logging for troubleshooting. * - `layerid`: Identifier for the map layer, needed for DOM element retrieval. * * Error Handling and Debugging: * - Includes additional logging and checks to address missing elements and potential errors. * - Detailed console warnings and errors facilitate debugging when `debug` mode is activated. * * Returns: * - `cityData`: An object containing the current city's name, associated feature ID, and optional DOM element. */ function findCurrCity() { let cityData = { name: '', featureId: '', domElement: null, // Initialize as null for safety }; // Get the current map center using wmeSDK const mapCenter = wmeSDK.Map.getMapCenter(); // Returns { lat: number, lon: number } const mapCenterPoint = [mapCenter.lon, mapCenter.lat]; // Check if _layer is defined and not null before proceeding if (!_layer || !_layer.length) { if (debug) console.warn(`${scriptName}: _layer is null or undefined. Unable to find current city.`); return cityData; } for (let i = 0; i < _layer.length; i++) { const feature = _layer[i]; const geometry = feature.geometry; const properties = feature.properties; const id = feature.id; // Check if the map center point is inside the feature's geometry (polygon) if (isPointInPolygon(mapCenterPoint, geometry.coordinates[0])) { cityData.name = properties.name; cityData.featureId = id; if (debug) { cityData.geojson = feature; } break; } } if (debug) { // Only attempt to get the DOM element if a valid featureId has been set if (cityData.featureId) { try { const currCityFeatureDomElement = wmeSDK.Map.getFeatureDomElement({ featureId: cityData.featureId, layerName: layerid, }); if (currCityFeatureDomElement !== null) { cityData.domElement = currCityFeatureDomElement; } else { console.warn(`${scriptName}: DOM element for feature ID ${cityData.featureId} not found.`); } } catch (error) { console.error(`${scriptName}: Error retrieving DOM element for feature ID ${cityData.featureId}:`, error); } } } if (debug) { console.log(`${scriptName}: Current Focused City Object is:`, cityData); } return cityData; } /** * Function: updateCitiesLayer * --------------------------- * Asynchronously updates the cities layer on the map based on the current state and zoom level, * ensuring proper display of city polygons and region names. * * Steps: * 1. Check the map's current zoom level and exit early if it's below 12, as detailed city view is unnecessary. * 2. Retrieve the top state from the map data model. If different from the current state (`currState`), * invoke `updateCityPolygons` to refresh city polygon data. * 3. Identify the current city using `findCurrCity`. Ensure the city data is valid before proceeding. * 4. Update the display name of the district or region using `updateDistrictNameDisplay`. * 5. Redraw the map layer to reflect the updated city data. * * Error Handling: * - Try-catch block used to handle any runtime errors gracefully, logging details for debugging. * - Checks for valid `currCity` and `currCity.name` to prevent operations on missing data. * * Globals: * - `scriptName`: Used for logging errors and operation details. * - `currState`: Tracks the name of the state currently being processed. * - `layerid`: Identifier for the target layer where cities are displayed. * - `currCity`: Object to store the currently identified city, utilized in display logic. */ async function updateCitiesLayer() { try { const zoom = wmeSDK.Map.getZoomLevel(); if (zoom < 12) { return; } const topState = wmeSDK.DataModel.States.getTopState(); if (!topState) { if (debug) console.log(`${scriptName}: topState is null. Skipping updateCityPolygons.`); return; } if (currState !== topState.name) { await updateCityPolygons(); } currCity = findCurrCity(); if (!currCity || !currCity.name) { if (debug) console.log(`${scriptName}: No Current city Polygon found for this location....`); return; } updateDistrictNameDisplay(); wmeSDK.Map.redrawLayer({ layerName: layerid }); } catch (error) { console.error(`${scriptName}: Error in updateCitiesLayer -`, error); } } function updateDistrictNameDisplay() { // Remove existing district name displays $('.wmecitiesoverlay-region').remove(); // Verify if _layer has features and a current city is specified if (Array.isArray(_layer) && _layer.length > 0 && currCity.name != '') { let color = '#00ffff'; // Create a new div element for displaying the current city var $div = $('<div>', { id: 'wmecitiesoverlay', class: 'wmecitiesoverlay-region', style: 'float:left; margin-left:10px;', }).css({ color: color, cursor: 'pointer', }); var $span = $('<span>').css({ display: 'inline-block' }); $span.text(currCity.name).appendTo($div); // Append the new element after the location-info-region $('.location-info-region').after($div); } } /** * Determines if a given point is inside a polygon using the ray-casting algorithm. * * This function checks whether a point, defined by its coordinates, is inside a polygon. * The polygon is represented by an array of vertices (points), and the function uses the * ray-casting technique to toggle the state whenever the ray crosses a polygon edge. * * @param {Array} point - An array [x, y] representing the coordinates of the point to test. * @param {Array} vs - An array of vertices, where each vertex is represented as [x, y]. * @returns {boolean} - True if the point is inside the polygon, false otherwise. **/ function isPointInPolygon(point, vs) { const [x, y] = point; let inside = false; for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { const [xi, yi] = vs[i]; const [xj, yj] = vs[j]; const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } async function fetch(url) { //return await $.get(url); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, method: 'GET', onload(res) { if (res.status < 400) { resolve(res.responseText); } else { reject(res); } }, onerror(res) { reject(res); }, }); }); } /** * Function: updateAllMaps * ----------------------- * Asynchronously updates KML data for all states in the current country, comparing * local storage against the latest content available in a GitHub repository. * * Steps: * 1. Get the top country from the map data model and retrieve its abbreviation. * 2. Fetch the keys for all states' city data stored locally. * 3. Determine the appropriate state abbreviation object based on the country's abbreviation. * 4. Retrieve the list of KML files from the GitHub repository, parsing the response. * 5. For each state in local storage, check if the KML file size differs from the server's version. * If so, fetch the updated KML file, update local storage, and cache if necessary. * 6. Log the count and names of states updated in the user's interface. * 7. Finally, refresh city layers using `updateCitiesLayer`. * * Note: * - Utilizes persistent local storage (`idbKeyval`) and caching (`kmlCache`) to reduce unnecessary data loads. * - Updates DOM element `#WMECOupdateStatus` to reflect operation results, aiding user interaction and feedback. * * Globals: * - `scriptName`: The name used for logging and user feedback. * - `repoOwner`: Identifier for the GitHub repository owner, used for URL generation. * - `currState`: Tracks the current state being processed, updated during KML fetching. * - `_kml`: Stores KML data when a matching state is currently active. * - `layerid`: Identifier for the map layer where updates are applied. * - `_US_States` and `_MX_States`: Objects managing state abbreviation lookup. * - `kmlCache`: Object to locally cache loaded KML data for efficient retrieval. */ async function updateAllMaps() { const topCountry = wmeSDK.DataModel.Countries.getTopCountry(); let countryAbbr = topCountry.abbr; let keys = await idbKeyval.keys(`${countryAbbr}_states_cities`); let updatedCount = 0; let updatedStates = ''; let countryAbbrObj; if (countryAbbr === 'US') countryAbbrObj = _US_States; else if (countryAbbr === 'MX') countryAbbrObj = _MX_States; let KMLinfoArr = await fetch(`https://api.github.com/repos/${repoOwner}/WME-Cities-Overlay/contents/KMLs/${countryAbbr}`); KMLinfoArr = $.parseJSON(KMLinfoArr); let state; for (let i = 0; i < keys.length; i++) { state = keys[i]; for (let j = 0; j < KMLinfoArr.length; j++) { if (KMLinfoArr[j].name === `${state}_Cities.kml`) { //check the size in db against server - if different, update db let stateObj = await idbKeyval.get(`${countryAbbr}_states_cities`, state); if (stateObj.kmlsize !== KMLinfoArr[j].size) { let kml = await fetch(`https://raw.githubusercontent.com/${repoOwner}/WME-Cities-Overlay/master/KMLs/${countryAbbr}/${state}_Cities.kml`); if (state === countryAbbrObj.getAbbreviation(currState)) _kml = kml; await idbKeyval.set(`${countryAbbr}_states_cities`, { kml: kml, state: state, kmlsize: KMLinfoArr[j].size, }); if (kmlCache[state] != null) kmlCache[state] = _kml; if (updatedStates != '') updatedStates += `, ${state}`; else updatedStates += state; updatedCount += 1; } break; } } } if (updatedCount > 0) $('#WMECOupdateStatus').text(`${updatedCount} state file${updatedCount > 1 ? 's' : ''} updated - ${updatedStates}`); else $('#WMECOupdateStatus').text('No updates available'); updateCitiesLayer(); } async function init() { initTab(); //I18n.translations[I18n.locale].layers.name[layerid] = "Cities Overlay"; const layerConfig = { styleRules: [ { predicate: () => true, style: { strokeDashstyle: 'solid', strokeColor: '${dynamicStrokeColor}', strokeOpacity: '${dynamicStrokeOpacity}', strokeWidth: '${dynamicStrokeWidth}', fillOpacity: '${dynamicFillOpacity}', fillColor: '${dynamicFillColor}', fontColor: '#ffffff', label: '${formatLabel}', labelOutlineColor: '#000000', labelOutlineWidth: 4, labelAlign: 'cm', fontSize: '16px', }, }, ], styleContext: { dynamicStrokeColor: (context) => { // Check if focused city highlighting is enabled and feature matches currCity if (_settings.HighlightFocusedCity && context.feature.id === currCity.featureId) { return '#f7ad25'; // Highlight stroke color } return _color; // Default stroke color }, dynamicFillColor: (context) => { // Check if focused city highlighting is enabled and feature matches currCity if (_settings.HighlightFocusedCity && context.feature.id === currCity.featureId) { return '#f7ad25'; // Highlight fill color } return _color; // Default fill color }, dynamicStrokeWidth: (context) => { // Increase stroke width if focused city highlighting is enabled and feature matches currCity if (_settings.HighlightFocusedCity && context.feature.id === currCity.featureId) { return 6; // Highlight stroke width } return 2; // Default stroke width }, dynamicStrokeOpacity: () => { return _settings.FillPolygons ? defaultStrokeOpacity : noFillStrokeOpacity; }, dynamicFillOpacity: () => { return _settings.FillPolygons ? defaultFillOpacity : 0; }, formatLabel: (context) => { let labelTemplate = ''; if (!_settings.ShowCityLabels) { return ''; // Skip rendering if disabled in settings } // Confirm necessary properties exist in the context if (!context || !context.feature || !context.feature.properties || !context.feature.properties.labelText) { console.error(`${scriptName}: Invalid context or missing 'labelText' property.`); return ''; } // Direct assignment of label text labelTemplate = context.feature.properties.labelText.trim(); return labelTemplate; // Return trimmed label for display }, }, }; wmeSDK.Map.addLayer({ layerName: layerid, styleRules: layerConfig.styleRules, styleContext: layerConfig.styleContext, zIndexing: true, }); // Set visibility to true for the layer wmeSDK.Map.setLayerVisibility({ layerName: layerid, visibility: _settings.layerVisible }); wmeSDK.LayerSwitcher.addLayerCheckbox({ name: 'Cities Overlay' }); wmeSDK.LayerSwitcher.setLayerCheckboxChecked({ name: 'Cities Overlay', isChecked: _settings.layerVisible }); wmeSDK.Events.on({ eventName: 'wme-layer-checkbox-toggled', eventHandler: layerToggled }); wmeSDK.Events.on({ eventName: 'wme-map-move-end', eventHandler: onMapMove }); if (_settings.layerVisible) { await updateCityPolygons(); currCity = findCurrCity(); if (_settings.AutoUpdateKMLs) { updateAllMaps(); } } } // END int() function function initTab() { // Create the section element using jQuery var $section = $('<div>', { style: 'padding:8px 16px', id: 'WMECitiesOverlaySettings', }); // Function to inject custom CSS function addCustomStyles() { const style = document.createElement('style'); style.textContent = ` .wmecoSettingsCheckbox { margin-right: 5px; /* Adds space to the right of the checkbox */ cursor: pointer; /* Pointer indicates interactivity */ appearance: none; /* Remove default styling */ width: 16px; /* Width of checkbox */ height: 16px; /* Height of checkbox */ background-color: #e0e0e0; /* Light gray background for unselected state */ border: 2px solid #bbb; /* Soft border */ border-radius: 4px; /* Slight rounded corners */ position: relative; /* Position relative for inner elements */ transition: all 0.3s ease; /* Smooth transition for hover effects */ box-shadow: 0 2px 5px rgba(0,0,0,0.15); /* Adds a subtle shadow */ } .wmecoSettingsCheckbox:hover { background-color: #d1d1d1; /* Slightly darker on hover */ border-color: #999; /* Darker border on hover */ margin-right: 5px; } .wmecoSettingsCheckbox:checked { background-color: #4caf50; /* Green background for checked state */ border-color: #3e8e41; /* Darker green border for checked */ margin-right: 5px; } .wmecoSettingsCheckbox:checked::after { content: ''; /* Content for checkmark */ position: absolute; left: 4px; /* Horizontal position for checkmark */ top: 0px; /* Vertical position for checkmark */ width: 12px; /* Width of checkmark */ height: 12px; /* Height of checkmark */ border: solid white; /* White checkmark */ border-width: 0 3px 3px 0; transform: rotate(45deg); /* Rotation to create checkmark */ margin-right: 5px; } `; document.head.appendChild(style); } // Append the HTML content to the section $section.append( `<h4 style="margin-bottom:0px;"> <i id="citiesPower" class="fa fa-power-off" aria-hidden="true" style="color:${_settings.layerVisible ? 'rgb(0,180,0)' : 'rgb(255, 0, 0)'}; cursor:pointer;"> </i> <b>WME Cities Overlay</b> </h4>`, `<h6 style="margin-top:0px;">${GM_info.script.version}</h6>`, '<div id="divWMECOFillPolygons"><input type="checkbox" id="_cbCOFillPolygons" class="wmecoSettingsCheckbox" /><label for="_cbCOFillPolygons">Fill polygons</label></div>', '<div id="divWMECOShowCityLabels"><input type="checkbox" id="_cbCOShowCityLabels" class="wmecoSettingsCheckbox" /><label for="_cbCOShowCityLabels">Show city labels</label></div>', '<div id="divWMECOHighlightFocusedCity"><input type="checkbox" id="_cbCOHighlightFocusedCity" class="wmecoSettingsCheckbox" /><label for="_cbCOHighlightFocusedCity">Highlight focused city</label></div>', '<fieldset id="fieldUpdates" style="border: 1px solid silver; padding: 8px; border-radius: 4px;">' + '<legend style="margin-bottom:0px; border-bottom-style:none;width:auto;"><h4>Update Settings</h4></legend>' + '<div id="divWMECOUpdateMaps" title="Checks for new state files for the current country"><button id="WMECOupdateMaps" type="button">Refresh / Update database</button></div>' + '<div id="WMECOupdateStatus"></div>' + '<div id="divWMECOAutoUpdateKMLs" title="Checks for updated state files for the current country when WME loads"><input type="checkbox" id="_cbCOAutoUpdateKMLs" class="wmecoSettingsCheckbox" /><label for="_cbCOAutoUpdateKMLs">Automatically update database</label></div>' + '</fieldset>' ); // Add styles addCustomStyles(); // Register the script tab with the sidebar wmeSDK.Sidebar.registerScriptTab() .then(({ tabLabel, tabPane }) => { // Set the tab label and title tabLabel.textContent = 'Cities'; tabLabel.title = scriptName; // Append the section to the tab pane tabPane.appendChild($section.get(0)); // Set initial checkbox states based on settings setChecked('_cbCOShowCityLabels', _settings.ShowCityLabels); setChecked('_cbCOFillPolygons', _settings.FillPolygons); setChecked('_cbCOHighlightFocusedCity', _settings.HighlightFocusedCity); setChecked('_cbCOAutoUpdateKMLs', _settings.AutoUpdateKMLs); // Add event listeners $('.wmecoSettingsCheckbox').change(function () { var settingName = $(this)[0].id.substr(5); _settings[settingName] = this.checked; saveSettings(); }); $('#citiesPower').click(function () { layerToggled(); }); $('#WMECOupdateMaps').click(updateAllMaps); $('#_cbCOFillPolygons').change(function () { _settings.FillPolygons = this.checked; saveSettings(); wmeSDK.Map.redrawLayer({ layerName: layerid }); }); $('#_cbCOShowCityLabels').change(function () { _settings.ShowCityLabels = this.checked; saveSettings(); wmeSDK.Map.redrawLayer({ layerName: layerid }); }); $('#_cbCOHighlightFocusedCity').change(function () { _settings.HighlightFocusedCity = this.checked; saveSettings(); wmeSDK.Map.redrawLayer({ layerName: layerid }); }); }) .catch((error) => { console.error(`${scriptName}: Error registering the script tab:`, error); }); } function onMapMove() { if (_settings.layerVisible) { updateCitiesLayer(); } } function layerToggled() { // Toggle the visibility state _settings.layerVisible = !_settings.layerVisible; const visible = _settings.layerVisible; wmeSDK.Map.setLayerVisibility({ layerName: layerid, visibility: visible }); wmeSDK.LayerSwitcher.setLayerCheckboxChecked({ name: 'Cities Overlay', isChecked: visible }); if (visible) { $('#citiesPower').css('color', 'rgb(0,180,0)'); // Add a custom event listener for visibility changes document.getElementById('citiesPower').addEventListener('visibilityChange', updateCitiesLayer); // Dispatch or trigger the custom event const visibilityChangeEvent = new Event('visibilityChange'); document.getElementById('citiesPower').dispatchEvent(visibilityChangeEvent); } else { $('#citiesPower').css('color', 'rgb(255, 0, 0)'); // Dark mode color $('.wmecitiesoverlay-region').remove(); // Remove existing district name displays // Remove the custom event listener when not visible document.getElementById('citiesPower').removeEventListener('visibilityChange', updateCitiesLayer); } saveSettings(); } /** * Function: updateCityPolygons * ---------------------------- * Asynchronously loads and updates city polygons for the top state on the map, * utilizing local storage and caching strategies to optimize data retrieval. * * Steps: * 1. Retrieve the current top state and check if it differs from `currState`. * If so, proceed to load new city polygon data. * 2. Clear existing features from the map layer to prepare for new data. * 3. Determine the state abbreviation based on the country's abbreviation. * 4. Check local storage for cached KML data; if absent, fetch from a remote * GitHub repository and cache it locally. * 5. Use the KML data to update the map layer's polygons with `updatePolygons`. * 6. Log the loading time and redraw the map layer to reflect updates. * * Note: * - Utilizes caching (`kmlCache`) and persistent storage (`idbKeyval`) for data * efficiency across function executions. * - Displays console logs and timers to assist with monitoring load times and debugging. * * Globals: * - `scriptName`: Name used for logging. * - `_kml`: KML data used for polygon updates. * - `currState`: Tracks the currently processed state name. * - `layerid`: Identifier for the target map layer. * - `repoOwner`: GitHub repository owner used for remote KML extraction. * - `debug`: Flag to enable detailed logging for troubleshooting. * - `_US_States` and `_MX_States`: Modules for state abbreviation lookup. * - `kmlCache`: Object to store loaded KML data for future use. */ async function updateCityPolygons() { const topState = wmeSDK.DataModel.States.getTopState(); if (!topState) { if (debug) console.warn(`${scriptName}: topState is null. Exiting update.`); return; } if (currState !== topState.name) { const topCountry = wmeSDK.DataModel.Countries.getTopCountry(); if (!topCountry) { if (debug) console.warn(`${scriptName}: topCountry is null. Exiting update.`); return; } // Start loading indicator console.log(`${scriptName}: Loading City Polygons for ${topState.name}`); console.time(`${scriptName}: Loaded City Polygons for ${topState.name} in`); // Clear all features from layer before loading new data wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: layerid }); currState = topState.name; let countryAbbr = topCountry.abbr; let stateAbbr; if (countryAbbr === 'US') stateAbbr = _US_States.getAbbreviation(currState); else if (countryAbbr === 'MX') stateAbbr = _MX_States.getAbbreviation(currState); if (typeof stateAbbr !== 'undefined') { if (typeof kmlCache[stateAbbr] === 'undefined') { // Try to retrieve state info from local storage var request = await idbKeyval.get(`${countryAbbr}_states_cities`, stateAbbr); if (!request) { // Fetch from GitHub if not found locally let kmlURL = `https://raw.githubusercontent.com/${repoOwner}/WME-Cities-Overlay/master/KMLs/${countryAbbr}/${stateAbbr}_Cities.kml`; if (debug) console.log(`${scriptName}: KML URL`, kmlURL); let kml = await fetch(kmlURL); _kml = kml; updatePolygons(); await idbKeyval.set(`${countryAbbr}_states_cities`, { kml: kml, state: stateAbbr, kmlsize: 0, }); kmlCache[stateAbbr] = _kml; // Cache KML data locally } else { _kml = request.kml; kmlCache[stateAbbr] = _kml; // Cache locally if already fetched updatePolygons(); } } else { _kml = kmlCache[stateAbbr]; updatePolygons(); } } // End loading indicator console.timeEnd(`${scriptName}: Loaded City Polygons for ${topState.name} in`); } else { wmeSDK.Map.redrawLayer({ layerName: layerid }); } } /** * Function: updatePolygons * ------------------------- * This function updates the map layer with GeoJSON features derived from a KML string, * replacing existing features and handling potential errors during feature addition. * * Steps: * 1. Convert the KML string into GeoJSON features and store them in the `_layer` variable. * 2. Remove all current features from the specified layer using `wmeSDK.Map`. * 3. Map these features with unique IDs based on their index to prepare them for loading. * 4. Attempt to add each feature to the target layer while tracking successes and errors. * 5. Populate the global `_layer` variable with successfully loaded features. * 6. Log the number of successfully added and skipped features and display loaded layers. * * Error Handling: * - Catch and log errors occurring during the feature removal and addition process. * - Differentiate between `InvalidStateError` for missing layers and `ValidationError` * for issues with feature data, providing specific details for troubleshooting. * * Globals: * - `scriptName`: Name used for logging. * - `_kml`: The KML string serving as the data source. * - `_layer`: Global state to track currently loaded features. * - `layerid`: Identifier for the target layer. * - `debug`: Flag to enable detailed logging for troubleshooting. */ function updatePolygons() { // Retrieve GeoJSON features from the KML string; conversion handled inside GetFeaturesFromKMLString _layer = GetFeaturesFromKMLString(_kml); // Remove all existing features from the specified layer try { wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: layerid }); if (debug) console.log(`${scriptName}: All features removed from layer: ${layerid}`); } catch (error) { console.error(`${scriptName}: Error removing features from layer: ${layerid}`, error); } // Map features array with unique index-based IDs const featuresToLoad = _layer.map((f, index) => ({ type: f.type, id: `${layerid}_${index}`, // Use feature index for uniqueness geometry: f.geometry, properties: f.properties, })); wmeSDK.Map.dangerouslyAddFeaturesToLayerWithoutValidation({ features: featuresToLoad, layerName: layerid, }); _layer = featuresToLoad; // populates the global _layer if (debug) console.log(`${scriptName}: Current State is ${currState}`); // Log completion console.log(`${scriptName}: ${featuresToLoad.length} Towns added`); if (debug) console.log(`${scriptName}: Layers Loaded are:`, _layer); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址