您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
WME GeoFile is a File Importer that allows you to import various geometry files (supported formats: GeoJSON, KML, WKT, GML, GPX, OSM, shapefiles(SHP,SHX,DBF).ZIP) into the Waze Map Editor (WME).
当前为
// ==UserScript== // @name WME GeoFile // @namespace https://github.com/JS55CT // @description WME GeoFile is a File Importer that allows you to import various geometry files (supported formats: GeoJSON, KML, WKT, GML, GPX, OSM, shapefiles(SHP,SHX,DBF).ZIP) into the Waze Map Editor (WME). // @version 2025.06.25.00 // @author JS55CT // @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://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @require https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.15.0/proj4-src.js // @require https://update.greasyfork.org/scripts/524747/1542062/GeoKMLer.js // @require https://update.greasyfork.org/scripts/527113/1538395/GeoKMZer.js // @require https://update.greasyfork.org/scripts/523986/1575829/GeoWKTer.js // @require https://update.greasyfork.org/scripts/523870/1534525/GeoGPXer.js // @require https://update.greasyfork.org/scripts/526229/1537672/GeoGMLer.js // @require https://update.greasyfork.org/scripts/526996/1537647/GeoSHPer.js // @connect tigerweb.geo.census.gov // @grant unsafeWindow // @grant GM_xmlhttpRequest // @license Waze Development and Editing Community License // ==/UserScript== /**************************************************************************************************** * This script adaptes and build off the great work of 'wme geometries' * original-author: Timbones * original-contributors: wlodek76, Twister-UK * Original-source: https://greasyfork.org/en/scripts/8129-wme-geometries/code?version=128453 ***************************************************************************************************/ /******** * TO DO LIST: * 1. Update Labels for line feachers for pathLabel? and pathLabelCurve? Need to understand installPathFollowingLabels() more. *********/ /* External Variables and Objects: GM_info: unsafeWindow: WazeWrap: external utility library for interacting with the Waze Map Editor environment. LZString: library used for compressing and decompressing strings. proj4: Proj4-src.js version 2.15.0 GeoWKTer, GeoGPXer, GeoGMLer, GeoKMLer, GeoKMZer, GeoSHPer external classes/functions used for parsing geospatial data formats. */ var geometries = function () { "use strict"; const scriptMetadata = GM_info.script; const scriptName = scriptMetadata.name; let geolist; let debug = false; let formats; let formathelp; let db; let groupToggler; let projectionMap = {}; function layerStoreObj(fileContent, color, fileext, filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, labelattribute, orgFileext) { this.fileContent = fileContent; this.color = color; this.fileext = fileext; this.filename = filename; this.fillOpacity = fillOpacity; this.fontsize = fontsize; this.lineopacity = lineopacity; this.linesize = linesize; this.linestyle = linestyle; this.labelpos = labelpos; this.labelattribute = labelattribute; this.orgFileext = orgFileext; } 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, }); // Wait for both WME and WazeWrap to be ready Promise.all([isWmeReady(), isWazeWrapReady()]) .then(() => { console.log(`${scriptName}: All dependencies are ready.`); // Correctly initialize formats and formathelp using the function const formatResults = createLayersFormats(); formats = formatResults.formats; formathelp = formatResults.formathelp; init(); }) .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) { 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) => { (function check(tries = 0) { if (unsafeWindow.WazeWrap && unsafeWindow.WazeWrap.Ready) { resolve(); } else if (tries < 1000) { setTimeout(() => { check(++tries); }, 500); } else { reject(`${scriptName}: WazeWrap took too long to load.`); } })(); }); } /********************************************************************* * loadLayers * * Loads all saved layers from the IndexedDB and processes each one asynchronously. * This function retrieves the entire set of stored layers, decompresses them, * and invokes the parsing function for each layer, enabling them to be rendered or manipulated further. * * Workflow Details: * 1. Logs the start of the loading process to the console. * 2. Initiates a read-only transaction with the IndexedDB to fetch all stored layers. * 3. If layers are available: * a. Displays a parsing message indicating processing is underway. * b. Iterates over each stored layer asynchronously, using `loadLayer` to fetch and decompress each layer individually. * c. Calls `parseFile` on each successfully loaded layer to process and render it. * d. Ensures the parsing message is hidden upon completion of all operations. * 4. Logs a message when no layers are present to be loaded. * * Error Handling: * - Logs and rejects any errors occurring during the IndexedDB retrieval process. * - Catches and logs errors for each specific layer processing attempt to avoid interrupting the overall loading sequence. * * @returns {Promise} - Resolves when all operations are complete, whether successful or encountering errors in parts. *************************************************************************/ async function loadLayers() { console.log(`${scriptName}: Loading Saved Layers...`); // Check local storage for any legacy layers if (localStorage.WMEGeoLayers !== undefined) { WazeWrap.Alerts.info(scriptName, "Old layers were found in local storage. These will be deleted. Please reload your files to convert them to IndexedDB storage."); localStorage.removeItem("WMEGeoLayers"); console.log(`${scriptName}: Old layers in local storage have been deleted. Please reload your files.`); } // Continue by loading layers stored in IndexedDB const transaction = db.transaction(["layers"], "readonly"); const store = transaction.objectStore("layers"); const request = store.getAll(); return new Promise((resolve, reject) => { request.onsuccess = async function () { const storedLayers = request.result || []; if (storedLayers.length > 0) { try { toggleParsingMessage(true); const layerPromises = storedLayers.map(async (storedLayer) => { try { const layer = await loadLayer(storedLayer.filename); if (layer) { parseFile(layer); } } catch (error) { console.error(`${scriptName}: Error processing layer:`, error); } }); await Promise.all(layerPromises); } finally { toggleParsingMessage(false); } } else { console.log(`${scriptName}: No layers to load.`); } resolve(); }; request.onerror = function (event) { console.error(`${scriptName}: Error loading layers from IndexedDB`, event.target.error); reject(new Error("Failed to load layers from database")); }; }); } /********************************************************************* * Fetches and decompresses a specified layer from the IndexedDB by its filename. * This function handles the retrieval of a single layer, decompressing its data for subsequent usage. * * @param {string} filename - The name of the file representing the layer to be loaded. * * Workflow Details: * 1. Initiates a read-only transaction with the IndexedDB to fetch layer data by the filename. * 2. On successful retrieval: * a. Decompresses the stored data using LZString and parses it back to its original form. * b. Resolves the promise with the full decompressed data object. * 3. If no data is found for the specified filename, resolves with `null`. * * Error Handling: * - Logs errors to the console if data retrieval from the database fails. * - Rejects the promise with an error if data fetching is unsuccessful. * * @returns {Promise<Object|null>} - Resolves with the decompressed layer object if successful, or `null` if not found. *************************************************************************/ async function loadLayer(filename) { const transaction = db.transaction(["layers"], "readonly"); const store = transaction.objectStore("layers"); const request = store.get(filename); return new Promise((resolve, reject) => { request.onsuccess = function () { const result = request.result; if (result) { // Decompress the entire stored object const decompressedFileObj = JSON.parse(LZString.decompress(result.compressedData)); resolve(decompressedFileObj); } else { resolve(null); } }; request.onerror = function (event) { console.error("Error retrieving layer:", event.target.error); reject(new Error("Failed to fetch layer data")); }; }); } /********************************************************************* * init * * Description: * Initializes the user interface for the "WME Geometries" sidebar tab in the Waze Map Editor. This function sets up * the DOM structure, styles, event listeners, and interactions necessary for importing and working with geometric * files and Well-Known Text (WKT) inputs. * * Parameters: * - This function does not take any direct parameters but interacts with global objects and the document's DOM. * * Behavior: * - Registers a new sidebar tab labeled "GEO" using Waze's userscript API. * - Builds a user interface dynamically, adding elements such as title sections, file inputs, and buttons for importing * and clearing WKT data. * - Configures event listeners for file input changes and button clicks to handle layer management and WKT drawing. * - Sets default styles and hover effects for UI components to enhance user experience. * - Displays information about available formats and coordinate systems to guide users in their inputs. * - Ensures that the existing layers are loaded upon initialization by calling a separate function, `loadLayers`. * *************************************************************************/ async function init() { console.log(`${scriptName}: Loading User Interface ...`); wmeSDK.Sidebar.registerScriptTab().then(({ tabLabel, tabPane }) => { tabLabel.textContent = "GEO"; tabLabel.title = `${scriptName}`; let geobox = document.createElement("div"); tabPane.appendChild(geobox); let geotitle = document.createElement("div"); geotitle.innerHTML = GM_info.script.name; geotitle.style.cssText = "text-align: center; font-size: 1.1em; font-weight: bold;"; geobox.appendChild(geotitle); let geoversion = document.createElement("div"); geoversion.innerHTML = "v " + GM_info.script.version; geoversion.style.cssText = "text-align: center; font-size: 0.9em;"; geobox.appendChild(geoversion); let hr = document.createElement("hr"); hr.style.cssText = "margin-top: 3px; margin-bottom: 3px; border: 0; border-top: 1px solid;"; geobox.appendChild(hr); geolist = document.createElement("ul"); geolist.style.cssText = "margin: 5px 0; padding: 5px;"; geobox.appendChild(geolist); let hr1 = document.createElement("hr"); hr1.style.cssText = "margin-top: 3px; margin-bottom: 3px; border: 0; border-top: 1px solid;"; geobox.appendChild(hr1); let geoform = document.createElement("form"); geoform.style.cssText = "display: flex; flex-direction: column; gap: 0px;"; geoform.id = "geoform"; geobox.appendChild(geoform); let fileContainer = document.createElement("div"); fileContainer.style.cssText = "position: relative; display: inline-block;"; let inputfile = document.createElement("input"); inputfile.type = "file"; inputfile.id = "GeometryFile"; inputfile.title = ".geojson, .gml or .wkt"; inputfile.style.cssText = "opacity: 0; position: absolute; top: 0; left: 0; width: 95%; height: 100%; cursor: pointer; pointer-events: none;"; fileContainer.appendChild(inputfile); let customLabel = createButton("Import GEO File", "#8BC34A", "#689F38", "#FFFFFF", "label", "GeometryFile"); fileContainer.appendChild(customLabel); geoform.appendChild(fileContainer); inputfile.addEventListener("change", addGeometryLayer, false); let notes = document.createElement("p"); notes.innerHTML = ` <b>Formats:</b><br> ${formathelp}<br> <b>EPSG:</b> <br> | 3035 | 3414 | 4214 | 4258 | 4267 | 4283 |<br> | 4326 | 25832 | 26901->26923 | 27700 |<br> | 32601->32660 | 32701->32760 |`; notes.style.cssText = "display: block; font-size: 0.9em; margin-left: 0px; margin-bottom: 0px;"; geoform.appendChild(notes); //ONLY LOAD THIS SECTION if TOP COUNTRY is the United State id = 235 */ const wmeTopContry = wmeSDK.DataModel.Countries.getTopCountry(); console.log(`${scriptName}: Top Level Coutry = ${wmeTopContry.name} | ${wmeTopContry.abbr}`); if ((wmeTopContry.abbr || "") === "US") { let hrElement0 = document.createElement("hr"); hrElement0.style.cssText = "margin: 5px 0; border: 0; border-top: 1px solid"; geoform.appendChild(hrElement0); let usCensusB = document.createElement("p"); usCensusB.innerHTML = ` <b><a href="https://tigerweb.geo.census.gov/tigerwebmain/TIGERweb_main.html" target="_blank"; text-decoration: underline;"> US Census Bureau: </a></b>`; usCensusB.style.cssText = "display: block; font-size: 0.9em; margin-left: 0px; margin-bottom: 0px;"; geoform.appendChild(usCensusB); // State Boundary Button const stateBoundaryButtonContainer = createButton("Draw State Boundary", "#E57373", "#D32F2F", "#FFFFFF", "input"); stateBoundaryButtonContainer.onclick = () => { drawBoundary("state"); }; geoform.appendChild(stateBoundaryButtonContainer); // County Boundary Button const countyBoundaryButtonContainer = createButton("Draw County Boundary", "#8BC34A", "#689F38", "#FFFFFF", "input"); countyBoundaryButtonContainer.onclick = () => { drawBoundary("county"); }; geoform.appendChild(countyBoundaryButtonContainer); // countySub Boundary Button const countySubBoundaryButtonContainer = createButton("Draw County Sub Boundary", "#42A5F5", "#1976D2", "#FFFFFF", "input"); countySubBoundaryButtonContainer.onclick = () => { drawBoundary("countysub"); }; geoform.appendChild(countySubBoundaryButtonContainer); // ZipCode Boundary Button const zipCodeBoundaryButtonContainer = createButton("Draw Zip Code Boundary", "#6F66D2", "#645CBD", "#FFFFFF", "input"); zipCodeBoundaryButtonContainer.onclick = () => { drawBoundary("zipcode"); }; geoform.appendChild(zipCodeBoundaryButtonContainer); const whatsInViewButtonContainer = createButton("Whats in View", "#BA68C8", "#9C27B0", "#FFFFFF", "input"); whatsInViewButtonContainer.onclick = () => { whatsInView(); }; geoform.appendChild(whatsInViewButtonContainer); } // END OF United State / US Census Bureau spacific inputs let hrElement1 = document.createElement("hr"); hrElement1.style.cssText = "margin: 5px 0; border: 0; border-top: 1px solid"; geoform.appendChild(hrElement1); let inputContainer = document.createElement("div"); inputContainer.style.cssText = "display: flex; flex-direction: column; gap: 5px; margin-top: 10px;"; let colorFontSizeRow = document.createElement("div"); colorFontSizeRow.style.cssText = "display: flex; justify-content: normal; align-items: center; gap: 0px;"; let input_color_label = document.createElement("label"); input_color_label.setAttribute("for", "color"); input_color_label.innerHTML = "Color: "; input_color_label.style.cssText = "font-weight: normal; flex-shrink: 0; margin-right: 5px;"; let input_color = document.createElement("input"); input_color.type = "color"; input_color.id = "color"; input_color.value = "#00bfff"; input_color.name = "color"; input_color.style.cssText = "width: 60px;"; let input_font_size_label = document.createElement("label"); input_font_size_label.setAttribute("for", "font_size"); input_font_size_label.innerHTML = "Font Size: "; input_font_size_label.style.cssText = "margin-left: 40px; font-weight: normal; flex-shrink: 0; margin-right: 5px;"; let input_font_size = document.createElement("input"); input_font_size.type = "number"; input_font_size.id = "font_size"; input_font_size.min = "0"; input_font_size.max = "20"; input_font_size.name = "font_size"; input_font_size.value = "12"; input_font_size.step = "1.0"; input_font_size.style.cssText = "width: 50px; text-align: center;"; colorFontSizeRow.appendChild(input_color_label); colorFontSizeRow.appendChild(input_color); colorFontSizeRow.appendChild(input_font_size_label); colorFontSizeRow.appendChild(input_font_size); inputContainer.appendChild(colorFontSizeRow); // Row for fill opacity input let fillOpacityRow = document.createElement("div"); fillOpacityRow.style.cssText = `display: flex; flex-direction: column;`; // Polygon Fill Opacity let input_fill_opacity_label = document.createElement("label"); input_fill_opacity_label.setAttribute("for", "fill_opacity"); input_fill_opacity_label.innerHTML = `Fill Opacity % [${(0.05 * 100).toFixed()}]`; input_fill_opacity_label.style.cssText = `font-weight: normal;`; let input_fill_opacity = document.createElement("input"); input_fill_opacity.type = "range"; input_fill_opacity.id = "fill_opacity"; input_fill_opacity.min = "0"; input_fill_opacity.max = "1"; input_fill_opacity.step = "0.01"; input_fill_opacity.value = "0.05"; input_fill_opacity.name = "fill_opacity"; input_fill_opacity.style.cssText = `width: 100%; appearance: none; height: 12px; border-radius: 5px; outline: none;`; // Thumb styling via CSS pseudo-elements const styleElement = document.createElement("style"); styleElement.textContent = ` input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; /* Thumb width */ height: 15px; /* Thumb height */ background: #808080; /* Thumb color */ cursor: pointer; /* Switch cursor to pointer when hovering the thumb */ border-radius: 50%; } input[type=range]::-moz-range-thumb { width: 15px; height: 15px; background: #808080; cursor: pointer; border-radius: 50%; } input[type=range]::-ms-thumb { width: 15px; height: 15px; background: #808080; cursor: pointer; border-radius: 50%; } `; document.head.appendChild(styleElement); // Initialize with the input color's current value and opacity let updateOpacityInputStyles = () => { let color = input_color.value; let opacityValue = input_fill_opacity.value; let rgbaColor = `rgba(${parseInt(color.slice(1, 3), 16)}, ${parseInt(color.slice(3, 5), 16)}, ${parseInt(color.slice(5, 7), 16)}, ${opacityValue})`; input_fill_opacity.style.backgroundColor = rgbaColor; input_fill_opacity.style.border = `2px solid ${color}`; }; updateOpacityInputStyles(); // Event listener to update the label dynamically input_fill_opacity.addEventListener("input", function () { input_fill_opacity_label.innerHTML = `Fill Opacity % [${Math.round(this.value * 100)}]`; updateOpacityInputStyles(); }); // Append elements to the fill opacity row fillOpacityRow.appendChild(input_fill_opacity_label); fillOpacityRow.appendChild(input_fill_opacity); // Append the fill opacity row to the input container inputContainer.appendChild(fillOpacityRow); // Section for line stroke settings let lineStrokeSection = document.createElement("div"); lineStrokeSection.style.cssText = `display: flex; flex-direction: column; margin-top: 10px;`; // Line stroke section label let lineStrokeSectionLabel = document.createElement("span"); lineStrokeSectionLabel.innerText = "Line Stroke Settings:"; lineStrokeSectionLabel.style.cssText = `font-weight: bold; margin-bottom: 10px;`; lineStrokeSection.appendChild(lineStrokeSectionLabel); // Line Stroke Size let lineStrokeSizeRow = document.createElement("div"); lineStrokeSizeRow.style.cssText = `display: flex; align-items: center;`; let line_stroke_size_label = document.createElement("label"); line_stroke_size_label.setAttribute("for", "line_size"); line_stroke_size_label.innerHTML = "Size:"; line_stroke_size_label.style.cssText = `font-weight: normal; margin-right: 5px;`; let line_stroke_size = document.createElement("input"); line_stroke_size.type = "number"; line_stroke_size.id = "line_size"; line_stroke_size.min = "0"; line_stroke_size.max = "10"; line_stroke_size.name = "line_size"; line_stroke_size.value = "1"; line_stroke_size.step = ".5"; line_stroke_size.style.cssText = `width: 50px;`; lineStrokeSizeRow.appendChild(line_stroke_size_label); lineStrokeSizeRow.appendChild(line_stroke_size); lineStrokeSection.appendChild(lineStrokeSizeRow); // Line Stroke Style let lineStrokeStyleRow = document.createElement("div"); lineStrokeStyleRow.style.cssText = `display: flex; align-items: center; gap: 10px; margin-top: 5px; margin-bottom: 5px;`; let line_stroke_types_label = document.createElement("span"); line_stroke_types_label.innerText = "Style:"; line_stroke_types_label.style.cssText = `font-weight: normal;`; lineStrokeStyleRow.appendChild(line_stroke_types_label); let line_stroke_types = [ { id: "solid", value: "Solid" }, { id: "dash", value: "Dash" }, { id: "dot", value: "Dot" }, ]; for (const type of line_stroke_types) { let radioContainer = document.createElement("div"); radioContainer.style.cssText = `display: flex; align-items: center; gap: 5px;`; let radio = document.createElement("input"); radio.type = "radio"; radio.id = type.id; radio.value = type.id; radio.name = "line_stroke_style"; radio.style.cssText = `margin: 0; vertical-align: middle;`; if (type.id === "solid") { radio.checked = true; } let label = document.createElement("label"); label.setAttribute("for", radio.id); label.innerHTML = type.value; label.style.cssText = `font-weight: normal; margin: 0; line-height: 1;`; radioContainer.appendChild(radio); radioContainer.appendChild(label); lineStrokeStyleRow.appendChild(radioContainer); } lineStrokeSection.appendChild(lineStrokeStyleRow); inputContainer.appendChild(lineStrokeSection); // Line Stroke Opacity let lineStrokeOpacityRow = document.createElement("div"); lineStrokeOpacityRow.style.cssText = `display: flex; flex-direction: column;`; let line_stroke_opacity_label = document.createElement("label"); line_stroke_opacity_label.setAttribute("for", "line_stroke_opacity"); line_stroke_opacity_label.innerHTML = "Opacity % [100]"; line_stroke_opacity_label.style.cssText = `font-weight: normal;`; let line_stroke_opacity = document.createElement("input"); line_stroke_opacity.type = "range"; line_stroke_opacity.id = "line_stroke_opacity"; line_stroke_opacity.min = "0"; line_stroke_opacity.max = "1"; line_stroke_opacity.step = ".05"; line_stroke_opacity.value = "1"; line_stroke_opacity.name = "line_stroke_opacity"; line_stroke_opacity.style.cssText = `width: 100%; appearance: none; height: 12px; border-radius: 5px; outline: none;`; const updateLineOpacityInputStyles = () => { let color = input_color.value; let opacityValue = line_stroke_opacity.value; let rgbaColor = `rgba(${parseInt(color.slice(1, 3), 16)}, ${parseInt(color.slice(3, 5), 16)}, ${parseInt(color.slice(5, 7), 16)}, ${opacityValue})`; line_stroke_opacity.style.backgroundColor = rgbaColor; line_stroke_opacity.style.border = `2px solid ${color}`; }; updateLineOpacityInputStyles(); line_stroke_opacity.addEventListener("input", function () { line_stroke_opacity_label.innerHTML = `Opacity % [${Math.round(this.value * 100)}]`; updateLineOpacityInputStyles(); }); input_color.addEventListener("input", () => { updateLineOpacityInputStyles(); updateOpacityInputStyles(); }); lineStrokeOpacityRow.appendChild(line_stroke_opacity_label); lineStrokeOpacityRow.appendChild(line_stroke_opacity); // Append the line stroke opacity row to the input container inputContainer.appendChild(lineStrokeOpacityRow); // Adding a horizontal break before Label Position let hrElement2 = document.createElement("hr"); hrElement2.style.cssText = `margin: 5px 0; border: 0; border-top: 1px solid`; inputContainer.appendChild(hrElement2); // Section for label position let labelPositionSection = document.createElement("div"); labelPositionSection.style.cssText = `display: flex; flex-direction: column;`; // Label position section label let labelPositionSectionLabel = document.createElement("span"); labelPositionSectionLabel.innerText = "Label Position Settings:"; labelPositionSectionLabel.style.cssText = `font-weight: bold; margin-bottom: 5px;`; labelPositionSection.appendChild(labelPositionSectionLabel); // Container for horizontal and vertical positioning options let labelPositionContainer = document.createElement("div"); labelPositionContainer.style.cssText = `display: flex; margin-left: 10px; gap: 80px;`; // Column for horizontal alignment let horizontalColumn = document.createElement("div"); horizontalColumn.style.cssText = `display: flex; flex-direction: column; gap: 5px;`; let horizontalLabel = document.createElement("span"); horizontalLabel.innerText = "Horizontal:"; horizontalLabel.style.cssText = `font-weight: normal;`; horizontalColumn.appendChild(horizontalLabel); let label_pos_horizontal = [ { id: "l", value: "Left" }, { id: "c", value: "Center" }, { id: "r", value: "Right" }, ]; for (const pos of label_pos_horizontal) { let radioHorizontalRow = document.createElement("div"); radioHorizontalRow.style.cssText = `display: flex; align-items: center; gap: 5px;`; let radio = document.createElement("input"); radio.type = "radio"; radio.id = pos.id; radio.value = pos.id; radio.name = "label_pos_horizontal"; radio.style.cssText = `margin: 0; vertical-align: middle;`; let label = document.createElement("label"); label.setAttribute("for", radio.id); label.innerHTML = pos.value; label.style.cssText = `font-weight: normal; margin: 0; line-height: 1;`; if (radio.id === "c") { radio.checked = true; } radioHorizontalRow.appendChild(radio); radioHorizontalRow.appendChild(label); horizontalColumn.appendChild(radioHorizontalRow); } // Column for vertical alignment let verticalColumn = document.createElement("div"); verticalColumn.style.cssText = `display: flex; flex-direction: column; gap: 5px;`; let verticalLabel = document.createElement("span"); verticalLabel.innerText = "Vertical:"; verticalLabel.style.cssText = `font-weight: normal;`; verticalColumn.appendChild(verticalLabel); let label_pos_vertical = [ { id: "t", value: "Top" }, { id: "m", value: "Middle" }, { id: "b", value: "Bottom" }, ]; for (const pos of label_pos_vertical) { let radioVerticalRow = document.createElement("div"); radioVerticalRow.style.cssText = `display: flex; align-items: center; gap: 5px;`; let radio = document.createElement("input"); radio.type = "radio"; radio.id = pos.id; radio.value = pos.id; radio.name = "label_pos_vertical"; radio.style.cssText = `margin: 0; vertical-align: middle;`; let label = document.createElement("label"); label.setAttribute("for", radio.id); label.innerHTML = pos.value; label.style.cssText = `font-weight: normal; margin: 0; line-height: 1;`; if (radio.id === "m") { radio.checked = true; } radioVerticalRow.appendChild(radio); radioVerticalRow.appendChild(label); verticalColumn.appendChild(radioVerticalRow); } // Append columns to the label position container labelPositionContainer.appendChild(horizontalColumn); labelPositionContainer.appendChild(verticalColumn); labelPositionSection.appendChild(labelPositionContainer); inputContainer.appendChild(labelPositionSection); geoform.appendChild(inputContainer); // Adding a horizontal break before the WKT input section let hrElement3 = document.createElement("hr"); hrElement3.style.cssText = `margin: 5px 0; border: 0; border-top: 1px solid`; geoform.appendChild(hrElement3); // New label for the Text Area for WKT input section let wktSectionLabel = document.createElement("div"); wktSectionLabel.innerHTML = 'WKT Input: (<a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry" target="_blank">WKT Format</a> )'; wktSectionLabel.style.cssText = `font-weight: bold; margin-bottom: 5px; margin-top: 5px; display: block;`; geoform.appendChild(wktSectionLabel); // Text Area for WKT input let wktContainer = document.createElement("div"); wktContainer.style.cssText = `display: flex; flex-direction: column; gap: 5px;`; // Input for WKT Name let input_WKT_name = document.createElement("input"); input_WKT_name.type = "text"; input_WKT_name.id = "input_WKT_name"; input_WKT_name.name = "input_WKT_name"; input_WKT_name.placeholder = "Name of WKT"; input_WKT_name.style.cssText = `padding: 8px; font-size: 1rem; border: 2px solid; border-radius: 5px; width: 100%; box-sizing: border-box;`; wktContainer.appendChild(input_WKT_name); // Text Area for WKT input let input_WKT = document.createElement("textarea"); input_WKT.id = "input_WKT"; input_WKT.name = "input_WKT"; input_WKT.placeholder = "POINT(X Y) LINESTRING (X Y, X Y,...) POLYGON(X Y, X Y, X Y,...) etc...."; input_WKT.style.cssText = `width: 100%; height: 10rem; min-height: 5rem; max-height: 40rem; padding: 8px; font-size: 1rem; border: 2px solid; border-radius: 5px; box-sizing: border-box; resize: vertical;`; // Restrict resizing to vertical wktContainer.appendChild(input_WKT); // Container for the buttons let buttonContainer = document.createElement("div"); buttonContainer.style.cssText = `display: flex; gap: 45px;`; let submit_WKT_btn = createButton("Import WKT", "#8BC34A", "#689F38", "#FFFFFF", "input"); submit_WKT_btn.id = "submit_WKT_btn"; submit_WKT_btn.title = "Import WKT Geometry to WME Layer"; submit_WKT_btn.addEventListener("click", draw_WKT); buttonContainer.appendChild(submit_WKT_btn); let clear_WKT_btn = createButton("Clear WKT", "#E57373", "#D32F2F", "#FFFFFF", "input"); clear_WKT_btn.id = "clear_WKT_btn"; clear_WKT_btn.title = "Clear WKT Geometry Input and Name"; clear_WKT_btn.addEventListener("click", clear_WKT_input); buttonContainer.appendChild(clear_WKT_btn); wktContainer.appendChild(buttonContainer); geoform.appendChild(wktContainer); // Append the container to the form // Add Toggle Button for Debug let debugToggleContainer = document.createElement("div"); debugToggleContainer.style.cssText = `display: flex; align-items: center; margin-top: 15px;`; let debugToggleLabel = document.createElement("label"); debugToggleLabel.style.cssText = `margin-left: 10px;`; const updateLabel = () => { debugToggleLabel.innerText = `Debug mode ${debug ? "ON" : "OFF"}`; }; let debugSwitchWrapper = document.createElement("label"); debugSwitchWrapper.style.cssText = `position: relative; display: inline-block; width: 40px; height: 20px; border: 1px solid #ccc; border-radius: 20px;`; let debugToggleSwitch = document.createElement("input"); debugToggleSwitch.type = "checkbox"; debugToggleSwitch.style.cssText = `opacity: 0; width: 0; height: 0;`; let switchSlider = document.createElement("span"); switchSlider.style.cssText = `position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px;`; let innerSpan = document.createElement("span"); innerSpan.style.cssText = `position: absolute; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%;`; switchSlider.appendChild(innerSpan); const updateSwitchState = () => { switchSlider.style.backgroundColor = debug ? "#8BC34A" : "#ccc"; innerSpan.style.transform = debug ? "translateX(20px)" : "translateX(0)"; }; debugToggleSwitch.checked = debug; updateLabel(); updateSwitchState(); debugToggleSwitch.addEventListener("change", () => { debug = debugToggleSwitch.checked; updateLabel(); updateSwitchState(); console.log(`${scriptName}: Debug mode is now ${debug ? "enabled" : "disabled"}`); }); debugSwitchWrapper.appendChild(debugToggleSwitch); debugSwitchWrapper.appendChild(switchSlider); debugToggleContainer.appendChild(debugSwitchWrapper); debugToggleContainer.appendChild(debugToggleLabel); geoform.appendChild(debugToggleContainer); console.log(`${scriptName}: User Interface Loaded!`); }); setupProjectionsAndTransforms(); wmeSDK.Events.on({ eventName: "wme-map-move-end", eventHandler: () => { const whatsInView = document.getElementById("WMEGeowhatsInViewMessage"); if (whatsInView) { // Call the update function to refresh the contents of the existing popup updateWhatsInView(whatsInView); } // If the message does not exist, do nothing }, }); try { await initDatabase(); // Now you can safely call functions that use the db console.log(`${scriptName}: IndexedDB initialized successfully!`); loadLayers(); } catch (error) { console.error(`${scriptName}: Application Initialization Error:`, error); } } function initDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open("GeometryLayersDB", 1); request.onupgradeneeded = function (event) { const db = event.target.result; if (!db.objectStoreNames.contains("layers")) { db.createObjectStore("layers", { keyPath: "filename" }); } }; request.onsuccess = function (event) { db = event.target.result; resolve(); }; request.onerror = function (event) { console.error("Failed to open IndexedDB:", event.target.error); reject(new Error("IndexedDB initialization failed")); }; }); } /**************************************************************************************** * setupProjectionsAndTransforms * * Initializes and registers coordinate reference systems (CRS) and their transformations * using the Proj4 library. Ensures a wide range of geographic projections are available * for accurate map rendering and interoperability. * * Function Workflow: * 1. **Projection Definitions**: * - Defines proj4-compatible string definitions for a variety of EPSG codes. * - Each projection is associated with properties like `units` and `maxExtent`. * - Includes global systems like WGS84 and a range of UTM zone projections. * 2. **Registration Process**: * - Registers each projection definition with the proj4 library. * - Projections are made available for use in geographic applications needing diverse CRS support. * 3. **Alias and Identifier Mapping**: * - Creates a `projectionMap` linking common CRS identifiers to EPSG codes. * - Utilizes template-based logic to generate multiple aliases for each projection, * simplifying reference and lookup across applications. * 4. **UTM Zones Configuration**: * - Automatically constructs and registers UTM zone projections for both hemispheres * using a loop to cover EPSG:326xx and EPSG:327xx series. * 5. **Logging and Debugging**: * - Provides comprehensive logging if debugging is enabled, listing registered projections * and their detailed definitions for verification and troubleshooting. * Notes: * - Ensure this function runs at initialization to make all defined projections and transformations * instantly available across your mapping application. * - Alerts or logs errors if prerequisites are missing or issues arise during setup. ****************************************************************************************/ function setupProjectionsAndTransforms() { // Define projection mappings with additional properties needed to create OpenLayers projections (units: , maxExtent: yx:) //definition: should be in proj4js format const projDefs = { "EPSG:4326": { definition: "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees", }, "EPSG:3857": { definition: "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs", }, "EPSG:900913": { definition: "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs", }, "EPSG:102100": { definition: "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs", }, "EPSG:4269": { definition: "+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees", }, "EPSG:4267": { definition: "+title=NAD27 +proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs", }, "EPSG:3035": { definition: "+title=ETRS89-extended / LAEA Europe +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:4258": { definition: "+title=ETRS89 (European Terrestrial Reference System 1989 +proj=longlat +ellps=GRS80 +no_defs +type=crs", }, "EPSG:25832": { definition: "+title=ETRS89 / UTM zone 32N +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:27700": { definition: "+title=OSGB36 / British National Grid +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.060 +units=m +no_defs", }, "EPSG:4283": { definition: "+title=GDA94 / Geocentric Datum of Australia 1994 +proj=longlat +ellps=GRS80 +no_defs +type=crs", }, "EPSG:4214": { definition: "+title=Beijing 1954 +proj=longlat +ellps=krass +towgs84=15.8,-154.4,-82.3,0,0,0,0 +no_defs +type=crs", }, "EPSG:3414": { definition: "+title=SVY21 / Singapore TM +proj=tmerc +lat_0=1.36666666666667 +lon_0=103.833333333333 +k=1 +x_0=28001.642 +y_0=38744.572 +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", }, // NAD 83 and by Zone "EPSG:26901": { definition: "+title=NAD83 / UTM zone 1N +proj=utm +zone=1 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26902": { definition: "+title=NAD83 / UTM zone 2N +proj=utm +zone=2 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26903": { definition: "+title=NAD83 / UTM zone 3N +proj=utm +zone=3 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26904": { definition: "+title=NAD83 / UTM zone 4N +proj=utm +zone=4 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26905": { definition: "+title=NAD83 / UTM zone 5N +proj=utm +zone=5 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26906": { definition: "+title=NAD83 / UTM zone 6N +proj=utm +zone=6 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26907": { definition: "+title=NAD83 / UTM zone 7N +proj=utm +zone=7 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26908": { definition: "+title=NAD83 / UTM zone 8N +proj=utm +zone=8 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26909": { definition: "+title=NAD83 / UTM zone 9N +proj=utm +zone=9 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26910": { definition: "+title=NAD83 / UTM zone 10N +proj=utm +zone=10 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26911": { definition: "+title=NAD83 / UTM zone 11N +proj=utm +zone=11 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26912": { definition: "+title=NAD83 / UTM zone 12N +proj=utm +zone=12 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26913": { definition: "+title=NAD83 / UTM zone 13N +proj=utm +zone=13 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26914": { definition: "+title=NAD83 / UTM zone 14N +proj=utm +zone=14 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26915": { definition: "+title=NAD83 / UTM zone 15N +proj=utm +zone=15 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26916": { definition: "+title=NAD83 / UTM zone 16N +proj=utm +zone=16 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26917": { definition: "+title=NAD83 / UTM zone 17N +proj=utm +zone=17 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26918": { definition: "+title=NAD83 / UTM zone 18N +proj=utm +zone=18 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26919": { definition: "+title=NAD83 / UTM zone 19N +proj=utm +zone=19 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26920": { definition: "+title=NAD83 / UTM zone 20N +proj=utm +zone=20 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26921": { definition: "+title=NAD83 / UTM zone 21N +proj=utm +zone=21 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26922": { definition: "+title=NAD83 / UTM zone 22N +proj=utm +zone=22 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, "EPSG:26923": { definition: "+title=NAD83 / UTM zone 23N +proj=utm +zone=23 +ellps=GRS80 +towgs84=-2,0,4,0,0,0,0 +units=m +no_defs +type=crs", }, }; // Add WGS 84 UTM Zones - Global UTM zones covering various longitudes for both hemispheres for (let zone = 1; zone <= 60; zone++) { projDefs[`EPSG:${32600 + zone}`] = { definition: `+proj=utm +zone=${zone} +datum=WGS84 +units=m +no_defs`, }; projDefs[`EPSG:${32700 + zone}`] = { definition: `+proj=utm +zone=${zone} +south +datum=WGS84 +units=m +no_defs`, }; } // Register the projections in proj4.defs for (const [epsg, { definition }] of Object.entries(projDefs)) { proj4.defs(epsg, definition); } // Logging the # of registered proj4 definitions if (debug) { const defsCount = Object.entries(proj4.defs).length; console.log(`${scriptName}: Number of proj4 definitions registered: ${defsCount}`); } projectionMap = { // WGS84 common aliases, same as EPSG:4326 CRS84: "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS84": "EPSG:4326", WGS84: "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:WGS84": "EPSG:4326", "WGS 84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:WGS_84": "EPSG:4326", "CRS WGS84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS_WGS84": "EPSG:4326", "CRS:WGS84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS:WGS84": "EPSG:4326", "CRS::WGS84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS::WGS84": "EPSG:4326", "CRS:84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS:84": "EPSG:4326", "CRS::84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS::84": "EPSG:4326", "CRS 84": "EPSG:4326", "urn:ogc:def:crs:OGC:1.3:CRS_84": "EPSG:4326", // NAD83 common aliases, same as EPSG:4269 "NAD 83": "EPSG:4269", "urn:ogc:def:crs:OGC:1.3:NAD_83": "EPSG:4269", NAD83: "EPSG:4269", "urn:ogc:def:crs:OGC:1.3:NAD83": "EPSG:4269", // ETRS89 / LAEA Europe common aliases, same as EPSG:3035 "ETRS 89": "EPSG:3035", "urn:ogc:def:crs:OGC:1.3:ETRS_89": "EPSG:3035", ETRS89: "EPSG:3035", "urn:ogc:def:crs:OGC:1.3:ETRS89": "EPSG:3035", // NAD27 common aliases, same as EPSG:4267 "NAD 27": "EPSG:4267", "urn:ogc:def:crs:OGC:1.3:NAD_27": "EPSG:4267", NAD27: "EPSG:4267", "urn:ogc:def:crs:OGC:1.3:NAD27": "EPSG:4267", }; const identifierTemplates = [ "EPSG:{{code}}", "urn:ogc:def:crs:EPSG:{{code}}", "urn:ogc:def:crs:OGC:1.3:EPSG:{{code}}", "EPSG::{{code}}", "urn:ogc:def:crs:EPSG::{{code}}", "urn:ogc:def:crs:OGC:1.3:EPSG::{{code}}", "CRS:{{code}}", "urn:ogc:def:crs:OGC:1.3:CRS:{{code}}", "CRS::{{code}}", "urn:ogc:def:crs:OGC:1.3:CRS::{{code}}", "CRS {{code}}", "urn:ogc:def:crs:OGC:1.3:CRS_{{code}}", "CRS{{code}}", "urn:ogc:def:crs:OGC:1.3:CRS{{code}}", ]; // Extract EPSG codes from the projDefs object const epsgCodes = Object.keys(projDefs).map((key) => key.split(":")[1]); epsgCodes.forEach((code) => { identifierTemplates.forEach((template) => { const identifier = template.replace("{{code}}", code); projectionMap[identifier] = `EPSG:${code}`; }); }); if (debug) console.log(`${scriptName}: projectionMap:`, projectionMap); } /**************************************************************************** * draw_WKT * * Description: * Parses user-supplied Well-Known Text (WKT) to create a geometric layer on the map using the wicket.js library for * reliable parsing. This function configures the layer with user-defined styling options and ensures the layer is * stored and displayed appropriately. * * Parameters: * - No explicit parameters are passed; all input is taken from DOM elements configured by the user. * * Behavior: * - Retrieves styling options and WKT input from predefined HTML elements. * - Checks for duplicate layer names to prevent adding layers with the same name to the map. * - Validates the presence of WKT input and handles errors related to parsing and conversion to GeoJSON format. * - Constructs a `layerStoreObj` containing the parsed GeoJSON data and its styling details. * - Uses `parseFile` to add the parsed and styled layer to the map. * - Compresses and stores the layer information in `localStorage` for persistence. * - Provides users with real-time feedback via console logs and alerts when debug mode is enabled or when errors arise. * * Notes: * - Utilizes global variables and functions such as `formats`, `storedLayers`, and `parseFile`. * - Relies on DOM elements and user inputs that need to be correctly set up in the environment for this function to work. *****************************************************************************/ function draw_WKT() { // Retrieve style and layer options let color = document.getElementById("color").value; let fillOpacity = document.getElementById("fill_opacity").value; let fontsize = document.getElementById("font_size").value; let lineopacity = document.getElementById("line_stroke_opacity").value; let linesize = document.getElementById("line_size").value; let linestyle = document.querySelector('input[name="line_stroke_style"]:checked').value; let layerName = document.getElementById("input_WKT_name").value.trim(); let labelpos = document.querySelector('input[name="label_pos_horizontal"]:checked').value + document.querySelector('input[name="label_pos_vertical"]:checked').value; // Check for empty layer name if (!layerName) { if (debug) console.error(`${scriptName}: WKT Input layer name cannot be empty.`); WazeWrap.Alerts.error(scriptName, "WKT Input layer name cannot be empty."); return; } // Attempt to check if the layer already exists using SDK const layerID = layerName.replace(/[^a-z0-9_-]/gi, "_"); try { // Try setting the visibility of the layer to check existence wmeSDK.Map.setLayerVisibility({ layerName: layerID, visibility: true, }); // If this succeeds, the layer already exists if (debug) console.error(`${scriptName}: Current layer name "${layerName}" already used!`); WazeWrap.Alerts.error(scriptName, `Current layer name "${layerName} " already used!`); return; } catch (error) { if (error.name === "InvalidStateError") { // Layer does not exist: it's safe to proceed further if (debug) console.log(`${scriptName}: Layer name ${layerName} does not exist, proceeding to parse file.`); } else { console.error(`${scriptName}: Error checking layer existence`, error); WazeWrap.Alerts.error(scriptName, `Error checking layer existence.\n${error.message}`); return; } } // Retrieve and validate WKT input let wktInput = document.getElementById("input_WKT").value.trim(); if (!wktInput) { if (debug) console.error(`${scriptName}: WKT input is empty.`); WazeWrap.Alerts.error(scriptName, "WKT input is empty."); return; } try { // Create an instance of GeoWKTer const geoWKTer = new GeoWKTer(); const wktString = geoWKTer.read(wktInput, layerName); const geojson = geoWKTer.toGeoJSON(wktString); // Convert to GeoJSON // Prepare the layer object and invoke parseFile, which handles layer creation const obj = new layerStoreObj(geojson, color, "GEOJSON", layerName, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "${Name}", "WKT"); parseFile(obj); } catch (error) { console.error(`${scriptName}: Error processing WKT input`, error); WazeWrap.Alerts.error(scriptName, `Error processing WKT input. Please check your input format.\n${error.message}`); } } // Clears the current contents of the textarea. function clear_WKT_input() { document.getElementById("input_WKT").value = ""; document.getElementById("input_WKT_name").value = ""; } /**************************************************************************** * Draws the boundary of a geographical region (state, county, or CountySub) using ArcGIS data and user-defined * formatting options. Retrieves and parses geographical data, applies specified styling, and checks for existing layers * to prevent duplicating the visualization on the map. Alerts are shown for operations and error handling. * * @param {string} item - A descriptor indicating the type of boundary to be drawn (e.g., "State", "County", "CountySub"). * * Function workflow: * 1. Collects styling options from HTML form elements, which include color, opacity, font size, line styles, and label positions. * 2. Utilizes `getArcGISdata` to fetch the GeoJSON representation of the specified boundary. * 3. Validates the fetched data to ensure features exist. If none are found, alerts the user. * 4. Checks the map for existing boundary visualizations to avoid duplicating them. * 5. If a valid and non-duplicate boundary is identified: * a. Constructs a new `layerStoreObj` using the fetched GeoJSON data and the user-defined styling options. * b. Calls `parseFile` to render the boundary onto the map. * 6. Logs messages and displays alerts about the success of the operation, duplicate layers, and any data retrieval errors. * * Error Handling: * - Logs errors to the console and displays alerts for data retrieval issues from the GIS service. * - Notifies the user if the retrieved data does not contain any features. * - Alerts on attempts to render already loaded boundaries. *****************************************************************************/ function drawBoundary(item) { if (debug) console.log(`drawBoundary called with item: ${item}`); // Retrieve styling options let color = document.getElementById("color").value; let fillOpacity = document.getElementById("fill_opacity").value; let fontsize = document.getElementById("font_size").value; let lineopacity = document.getElementById("line_stroke_opacity").value; let linesize = document.getElementById("line_size").value; let linestyle = document.querySelector('input[name="line_stroke_style"]:checked').value; let labelpos = document.querySelector('input[name="label_pos_horizontal"]:checked').value + document.querySelector('input[name="label_pos_vertical"]:checked').value; // Get boundary geoJSON from is US Census Bureau getArcGISdata(item) .then((geojson) => { if (!geojson || !geojson.features || geojson.features.length === 0) { console.log(`No ${item} Boundary Available, Sorry!`); WazeWrap.Alerts.info(scriptName, `No ${item} Boundary Available, Sorry!`); return; } // Extract the first feature, assuming that's the desired state boundary for simplicity const Feature = geojson.features[0]; let layerName; if (item === "zipcode") { layerName = `ZIP CODE: ${Feature.properties.BASENAME}`; } else { layerName = Feature.properties.NAME; } const layerID = layerName.replace(/[^a-z0-9_-]/gi, "_"); try { // Attempt to set the visibility of the layer using the SDK to check if it exists wmeSDK.Map.setLayerVisibility({ layerName: layerID, visibility: true, }); // If successful, the layer already exists if (debug) console.log(`${scriptName}: current ${item} "${layerName}" boundary already loaded`); WazeWrap.Alerts.error(scriptName, `Current ${item} "${layerName}" Boundary already Loaded!`); return; } catch (error) { if (error.name === "InvalidStateError") { // Layer does not exist: it's safe to proceed further if (debug) console.log(`${scriptName}: Layer "${layerName}" does not exist, proceeding to parse file.`); } else { console.error(`${scriptName}: Error checking layer existence`, error); WazeWrap.Alerts.error(scriptName, `Error checking layer existence.\n${error.message}`); return; } } // Create the layer object and invoke parseFile since the layer does not exist const obj = new layerStoreObj(geojson, color, "GEOJSON", layerName, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, `${layerName}`, "GEOJSON"); parseFile(obj); }) .catch((error) => { console.error(`${scriptName}: Failed to retrieve ${item} boundary:`, error); WazeWrap.Alerts.error(scriptName, `Failed to retrieve ${item} boundary.`); }); } /************************************************************************* * addGeometryLayer * * Description: * Facilitates the addition of a new geometry layer to the map by reading a user-selected file, parsing its contents, * and configuring the layer with specified styling options. The function also updates UI elements and handles storage * of the layer information. * * Process: * - Captures a file from a user's file input and determines its extension and name. * - Collects styling and configuration options from the user interface through DOM elements. * - Validates user-selected file format against supported formats, handling any unsupported formats with an error message. * - Leverages a `FileReader` to asynchronously read the file's contents and creates a `fileObj`. * - Calls `parseFile` to interpret `fileObj`, creating and configuring the geometry layers on the map. * - Updates persistent storage with compressed data to save the state of added geometrical layers. * * Notes: * - Operates within a larger system context, relying on global variables such as `formats` for file format validation. *************************************************************************/ function addGeometryLayer() { const fileList = document.getElementById("GeometryFile"); const file = fileList.files[0]; fileList.value = ""; const fileName = file.name; const lastDotIndex = fileName.lastIndexOf("."); const fileext = lastDotIndex !== -1 ? fileName.substring(lastDotIndex + 1).toUpperCase() : ""; const filename = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName; // Collect configuration options from UI const color = document.getElementById("color").value; const fillOpacity = document.getElementById("fill_opacity").value; const fontsize = document.getElementById("font_size").value; const lineopacity = document.getElementById("line_stroke_opacity").value; const linesize = document.getElementById("line_size").value; const linestyle = document.querySelector('input[name="line_stroke_style"]:checked').value; const labelpos = document.querySelector('input[name="label_pos_horizontal"]:checked').value + document.querySelector('input[name="label_pos_vertical"]:checked').value; const reader = new FileReader(); reader.onload = function (e) { requestAnimationFrame(() => { try { let fileObj; switch (fileext) { case "ZIP": if (debug) console.log(`${scriptName}: .ZIP shapefile file found, converting to GEOJSON...`); if (debug) console.time(`${scriptName}: .ZIP shapefile conversion in`); const geoSHPer = new GeoSHPer(); (async () => { try { toggleParsingMessage(true); // turned off in parseFile() await geoSHPer.read(e.target.result); const SHPgeoJSON = geoSHPer.toGeoJSON(); if (debug) console.timeEnd(`${scriptName}: .ZIP shapefile conversion in`); const fileObj = new layerStoreObj(SHPgeoJSON, color, "GEOJSON", filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", "SHP"); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("ZIP shapefile")(error); } })(); break; case "WKT": try { // WKT files are assumed to be in projection WGS84 = EPSG:4326 if (debug) console.log(`${scriptName}: .WKT file found, converting to GEOJSON...`); if (debug) console.time(`${scriptName}: .WKT conversion in`); toggleParsingMessage(true); // turned off in parseFile() const geoWKTer = new GeoWKTer(); const wktDoc = geoWKTer.read(e.target.result, filename); const WKTgeoJSON = geoWKTer.toGeoJSON(wktDoc); if (debug) console.timeEnd(`${scriptName}: .WKT conversion in`); fileObj = new layerStoreObj(WKTgeoJSON, color, "GEOJSON", filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", fileext); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("WKT conversion")(error); } break; case "GPX": //The GPX format is inherently based on the WGS 84 coordinate system (EPSG:4326) try { if (debug) console.log(`${scriptName}: .GPX file found, converting to GEOJSON...`); if (debug) console.time(`${scriptName}: .GPX conversion in`); toggleParsingMessage(true); // turned off in parseFile() const geoGPXer = new GeoGPXer(); const gpxDoc = geoGPXer.read(e.target.result); const GPXtoGeoJSON = geoGPXer.toGeoJSON(gpxDoc); if (debug) console.timeEnd(`${scriptName}: .GPX conversion in`); fileObj = new layerStoreObj(GPXtoGeoJSON, color, "GEOJSON", filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", fileext); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("KML conversion")(error); } break; case "KML": //Represent geographic data and are natively based on the WGS 84 coordinate system (EPSG:4326) try { if (debug) console.log(`${scriptName}: .KML file found, converting to GEOJSON...`); if (debug) console.time(`${scriptName}: .KML conversion in`); toggleParsingMessage(true); // turned off in parseFile() const geoKMLer = new GeoKMLer(); const kmlDoc = geoKMLer.read(e.target.result); const KMLtoGeoJSON = geoKMLer.toGeoJSON(kmlDoc, true); if (debug) console.timeEnd(`${scriptName}: .KML conversion in`); fileObj = new layerStoreObj(KMLtoGeoJSON, color, "GEOJSON", filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", fileext); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("KML conversion")(error); } break; case "KMZ": //Represent geographic data and are natively based on the WGS 84 coordinate system (EPSG:4326) if (debug) console.log(`${scriptName}: .KMZ file found, extracting .KML files...`); if (debug) console.time(`${scriptName}: .KMZ conversion in`); toggleParsingMessage(true); // turned off in parseFile() const geoKMZer = new GeoKMZer(); (async () => { try { // Read and parse the KMZ file const kmlContentsArray = await geoKMZer.read(e.target.result); // Iterate over each KML file extracted from the KMZ kmlContentsArray.forEach(({ filename: kmlFile, content }, index) => { // Construct unique filenames for each KML file const uniqueFilename = kmlContentsArray.length > 1 ? `${filename}_${index + 1}` : `${filename}`; if (debug) console.log(`${scriptName}: Converting extracted .KML to GEOJSON...`); const geoKMLer = new GeoKMLer(); const kmlDoc = geoKMLer.read(content); const KMLtoGeoJSON = geoKMLer.toGeoJSON(kmlDoc, true); if (debug) console.timeEnd(`${scriptName}: .KMZ conversion in`); fileObj = new layerStoreObj(KMLtoGeoJSON, color, "GEOJSON", uniqueFilename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", "KMZ"); parseFile(fileObj); }); } catch (error) { toggleParsingMessage(false); handleError("KMZ read operation")(error); } })(); break; case "GML": try { if (debug) console.log(`${scriptName}: .GML file found, converting to GEOJSON...`); if (debug) console.time(`${scriptName}: .GML conversion in`); toggleParsingMessage(true); // turned off in parseFile() const geoGMLer = new GeoGMLer(); const gmlDoc = geoGMLer.read(e.target.result); const GMLtoGeoJSON = geoGMLer.toGeoJSON(gmlDoc); if (debug) console.timeEnd(`${scriptName}: .GML conversion in`); fileObj = new layerStoreObj(GMLtoGeoJSON, color, "GEOJSON", filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", fileext); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("GML conversion")(error); } break; case "GEOJSON": try { if (debug) console.log(`${scriptName}: .GEOJSON file found...`); toggleParsingMessage(true); // turned off in parseFile() const geojsonData = JSON.parse(e.target.result); // Parse the .GEOJSON file content as a JSON object fileObj = new layerStoreObj(geojsonData, color, fileext, filename, fillOpacity, fontsize, lineopacity, linesize, linestyle, labelpos, "", fileext); parseFile(fileObj); } catch (error) { toggleParsingMessage(false); handleError("GEOJSON parsing")(error); } break; default: toggleParsingMessage(false); handleError("unsupported file type")(new Error("Unsupported file type")); break; } } catch (error) { toggleParsingMessage(false); handleError("file")(error); } }); }; if (fileext === "ZIP" || fileext === "KMZ") { reader.readAsArrayBuffer(file); } else { reader.readAsText(file); } function handleError(context) { return (error) => { console.error(`${scriptName}: Error parsing ${context}:`, error); WazeWrap.Alerts.error(scriptName, `${error}`); }; } } /**************************************************************************************** * transformGeoJSON * * Transforms the coordinates of a GeoJSON object from a source CRS to a target CRS using proj4. * Validates CRS formats and ensures their definitions exist in proj4 before proceeding. * * Parameters: * @param {Object} geoJSON - The GeoJSON object to transform, which can be a FeatureCollection, * Feature, GeometryCollection, or Geometry. * @param {string} sourceCRS - The source Coordinate Reference System, formatted as 'EPSG:####'. * @param {string} targetCRS - The target Coordinate Reference System, formatted as 'EPSG:####'. * * Returns: * @returns {Object} - The transformed GeoJSON object with updated coordinates. * * Workflow: * - Validates CRS formats and checks for their existence in proj4 definitions. * - Depending on GeoJSON type, recursively applies coordinate conversion. * - Updates the CRS information within the GeoJSON to reflect the target CRS. ****************************************************************************************/ function transformGeoJSON(geoJSON, sourceCRS, targetCRS) { if (debug) console.log(`${scriptName}: transformGeoJSON() called with SourceCRS = ${sourceCRS} and TargetCRS = ${targetCRS}`); const isValidCRS = (crs) => typeof crs === "string" && /^EPSG:\d{4,5}$/.test(crs); if (!isValidCRS(sourceCRS) || !isValidCRS(targetCRS)) { console.error(`${scriptName}: Invalid CRS format detected: sourceCRS: ${sourceCRS}, targetCRS: ${targetCRS}`); throw new Error("Coordinates Reference Systems must be formatted as a string 'EPSG:####'."); } if (!proj4.defs[sourceCRS]) { console.error(`${scriptName}: Source CRS ${sourceCRS} is not defined in proj4.`); throw new Error(`Source CRS ${sourceCRS} is not defined in proj4.`); } if (!proj4.defs[targetCRS]) { console.error(`${scriptName}: Target CRS ${targetCRS} is not defined in proj4.`); throw new Error(`Target CRS ${targetCRS} is not defined in proj4.`); } 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; // Normalize the feacher "type" at the root level to valid geoJSON geoJSON.type = updateGeoJSONType(geoJSON.type); if (geoJSON.type === "FeatureCollection") { geoJSON.features = flattenGeoJSON(geoJSON.features, sourceCRS, targetCRS); } else if (geoJSON.type === "Feature") { geoJSON.geometry = flattenGeometry(geoJSON.geometry, sourceCRS, targetCRS); } else if (geoJSON.type === "GeometryCollection") { geoJSON.geometries = geoJSON.geometries.map((geometry) => flattenGeometry(geometry, sourceCRS, targetCRS)); } geoJSON.crs = { type: "name", properties: { name: targetCRS, }, }; return geoJSON; } /**************************************************************************************** * flattenGeoJSON * * Converts complex geometries (MultiPoint, MultiLineString, MultiPolygon) into simpler forms * (Point, LineString, Polygon) and applies coordinate transformations. * * Parameters: * @param {Array} features - The array of features from the GeoJSON FeatureCollection. * @param {string} sourceCRS - The source Coordinate Reference System, formatted as 'EPSG:####'. * @param {string} targetCRS - The target Coordinate Reference System, formatted as 'EPSG:####'. * * Returns: * @returns {Array} - The array of flattened and transformed features. ****************************************************************************************/ function flattenGeoJSON(features, sourceCRS, targetCRS) { return features.flatMap((feature) => { //featureIndex - JS55CT const flattenedGeometries = []; geomEach(feature.geometry, (geometry) => { const type = geometry === null ? null : geometry.type; switch (type) { case "Point": case "LineString": case "Polygon": flattenedGeometries.push({ type: "Feature", geometry: { type: type, coordinates: convertCoordinates(sourceCRS, targetCRS, geometry.coordinates), }, properties: feature.properties, }); break; case "MultiPoint": case "MultiLineString": case "MultiPolygon": const geomType = type.split("Multi")[1]; geometry.coordinates.forEach((coordinate) => { flattenedGeometries.push({ type: "Feature", geometry: { type: geomType, coordinates: convertCoordinates(sourceCRS, targetCRS, coordinate), }, properties: feature.properties, }); }); break; case "GeometryCollection": geometry.geometries.forEach((geom) => { flattenedGeometries.push({ type: "Feature", geometry: { type: geom.type, coordinates: convertCoordinates(sourceCRS, targetCRS, geom.coordinates), }, properties: feature.properties, }); }); break; default: throw new Error(`Unknown Geometry Type: ${type}`); } }); return flattenedGeometries; }); } /**************************************************************************************** * geomEach * * Iterates over each geometry in a feature to handle different types and coordinate structures. * * Parameters: * @param {Object} geometry - The geometry object extracted from a feature. * @param {Function} callback - A callback function to execute for each geometry type. ****************************************************************************************/ 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: coordinate, }); }); break; case "GeometryCollection": geometry.geometries.forEach(callback); break; default: throw new Error(`Unknown Geometry Type: ${type}`); } } /**************************************************************************************** * convertCoordinates * * Converts coordinates from a source CRS to a target CRS using proj4. Handles different * coordinate structures like points, lines, and polygons recursively if necessary. * * Parameters: * @param {string} sourceCRS - The CRS of the input coordinates, as 'EPSG:####'. * @param {string} targetCRS - The CRS to convert the coordinates to, as 'EPSG:####'. * @param {Array} coordinates - Array representing the coordinates to be converted. This * could be a single point [x, y] or recursive arrays for lines and polygons. * * Returns: * @returns {Array} - The converted coordinates with Z & M dimension removed if present. * * Workflow: * - Recursively converts coordinate arrays for complex geometries. * - Applies proj4 transformation to single points. ****************************************************************************************/ function convertCoordinates(sourceCRS, targetCRS, coordinates) { // Function to strip Z coordinates function stripZ(coords) { if (Array.isArray(coords[0])) { return coords.map(stripZ); } else { return coords.slice(0, 2); // Return only X, Y } } const strippedCoords = stripZ(coordinates); // Handle multi-point, line, or polygon coordinates recursively if needed if (Array.isArray(strippedCoords[0])) { return strippedCoords.map((coordinate) => convertCoordinates(sourceCRS, targetCRS, coordinate)); } // Handle single point coordinates if (typeof strippedCoords[0] === "number") { const [x, y] = strippedCoords; // Destructure into x and y // Convert the single point using proj4 const convertedPoint = proj4(sourceCRS, targetCRS, [x, y]); return convertedPoint; } console.warn(`${scriptName}: Unsupported coordinate format detected. Returning coordinates unchanged.`); return strippedCoords; } /**************************************************************************************** * parseFile * * Processes geographic data from a file object, applying styles and adding it as a vector * layer to the map. Handles projections and updates the UI based on file loading and * parsing outcomes. * * Parameters: * @param {Object} fileObj - Contains file data and styling configurations. * - {string} fileObj.filename - Name of the file. * - {string} fileObj.fileext - File extension to select the parser. * - {string} fileObj.fileContent - File's geographic content. * - {string} fileObj.color - Color for styling. * - {number} fileObj.lineopacity, fileObj.linesize, fileObj.linestyle - Line styling. * - {number} fileObj.fillOpacity - Opacity for fill areas. * - {number} fileObj.fontsize - Font size for labels. * - {string} fileObj.labelpos - Label anchor position. * - {string} fileObj.labelattribute - Attribute for labeling features. * * Workflow: * - Validates parser selection based on file extension. * - Determines source CRS from file content, defaulting to EPSG:4326 if not specified. * - Uses proj4 to transform the GeoJSON features when needed, targeting EPSG:3857. * - Parses features using the appropriate parser and handles errors. * - Configures labeling based on given attribute or prompts the user for selection. * - Updates map with the styled layer composed of parsed features. * - Manages user interface updates regarding file processing status. ****************************************************************************************/ function parseFile(fileObj) { if (debug) console.log(`${scriptName}: parseFile(): called with input of:`, fileObj); //const fileext = fileObj.fileext.toUpperCase(); const orgFileext = fileObj.orgFileext.toUpperCase(); const fileContent = fileObj.fileContent; const filename = fileObj.filename; // Initialize sourceCRS to a default value (e.g., EPSG:4326) to ensure it's always defined let sourceCRS = "EPSG:4326"; // Default CRS if one can't be loacated in the GeoJSON most common one if (fileContent.crs && fileContent.crs.properties && fileContent.crs.properties.name) { const projection = fileContent.crs.properties.name; let mappedCRS = projectionMap[projection]; if (mappedCRS) { sourceCRS = mappedCRS; // Update only if a mapping exists if (debug) console.log(`${scriptName}: External Projection found in file: ${projection} was mapped to: ${sourceCRS}`); } else { const supportedProjections = "EPSG:3035|3414|4214|4258|4267|4283|4326|25832|26901->26923|27700|32601->32660|32701->32760| "; const message = `Found unsupported projection: ${projection}. <br>Supported projections are: <br>${supportedProjections}. <br>Cannot proceed without a supported projection.`; console.error(`${scriptName}: Error - ${message}`); WazeWrap.Alerts.error(scriptName, message); return; } } else { const message = "No External projection found. <br>Defaulting to EPSG:4326 (WGS 84)."; if (debug) { console.warn(`${scriptName}: Warning - ${message}`); WazeWrap.Alerts.info(scriptName, message); } } let featuresSDK; try { const targetCRS = "EPSG:4326"; //New WME SDK uses EPSG:4326 // Transform CRS , remove Z & M, flatten geoJSON if needed! const geoJSONToParse = transformGeoJSON(fileContent, sourceCRS, targetCRS); // NEED TO ADD flatten LOGIC HERE featuresSDK = geoJSONToParse.features; if (featuresSDK.length === 0) { toggleParsingMessage(false); console.error(`${scriptName}: No features found in transformed GeoJSON for ${filename}.${orgFileext}.`); WazeWrap.Alerts.error(`${scriptName}: No features found in transformed GeoJSON for ${filename}.${orgFileext}.`); return; } if (debug) console.log(`${scriptName}: Found ${featuresSDK.length} features for ${filename}.${orgFileext}.`); } catch (error) { toggleParsingMessage(false); console.error(`${scriptName}: Error parsing GeoJSON for ${filename}.${orgFileext}:`, error); WazeWrap.Alerts.error(`${scriptName}: Error parsing GeoJSON for ${filename}.${orgFileext}:\n${error}`); return; } toggleParsingMessage(false); // turned on in parsefile() if (fileObj.labelattribute) { createLayerWithLabelSDK(fileObj, featuresSDK, sourceCRS); } else { if (Array.isArray(featuresSDK)) { if (debug) console.log(`${scriptName}: Sample features objects:`, featuresSDK.slice(0, 10)); presentFeaturesAttributesSDK(featuresSDK.slice(0, 50), featuresSDK.length) .then((selectedAttribute) => { if (selectedAttribute) { fileObj.labelattribute = selectedAttribute; console.log(`${scriptName}: Label attribute selected: ${fileObj.labelattribute}`); createLayerWithLabelSDK(fileObj, featuresSDK, sourceCRS); } }) .catch((cancelReason) => { console.warn(`${scriptName}: User cancelled attribute selection and import: ${cancelReason}`); }); } } } /****************************************************************************************** * createLayerWithLabelSDK * * Description: * Configures and adds a new vector layer to the map using the WME SDK, applying styling and * dynamic labeling based on attributes from geographic features. This function handles the label * styling context, constructs the layer using SDK capabilities, updates the UI with toggler * controls, and stores the layer configuration in IndexedDB to preserve its state across sessions. * * Parameters: * @param {Object} fileObj - Contains metadata and styling options for the layer. * - {string} filename - Name of the file, sanitized and used for layer ID. * - {string} color - Color for layer styling. * - {number} lineopacity - Opacity for line styling. * - {number} linesize - Width of lines in the layer. * - {string} linestyle - Dash style for lines. * - {number} fillOpacity - Opacity for filling geometries. * - {number} fontsize - Font size for labels and points. * - {string} labelattribute - Template string for labeling features with `${attribute}` syntax. * - {string} labelpos - Position for label text alignment. * @param {Array} features - Array of geographic features to be added to the layer. * @param {Object} externalProjection - Projection object for transforming feature coordinates. * * Behavior: * - Constructs a label context to format and position labels based on feature attributes. * - Defines layer styling using attributes from `fileObj` and assigns style context for dynamic label computation. * - Creates a vector layer using the SDK, setting its unique ID from the sanitized filename. * - Uses SDK to manage layer visibility and adds geographic features to the layer. * - Registers the layer with a group toggler for UI controls to manage its visibility. * - Integrates the layer into the main map and manages associated UI elements for toggling. * - Prevents duplicate storage by checking existing layers, updating IndexedDB storage only for new layers. ******************************************************************************************/ async function createLayerWithLabelSDK(fileObj, features, externalProjection) { toggleLoadingMessage(true); // Show the user the loading message! const delayDuration = 300; setTimeout(async () => { try { let labelContext = { formatLabel: (context) => { let labelTemplate = fileObj.labelattribute; if (!labelTemplate || labelTemplate.trim() === "") { return ""; } labelTemplate = labelTemplate.replace(/\\n/g, "\n").replace(/<br\s*\/?>/gi, "\n"); if (!labelTemplate.includes("${")) { return labelTemplate; } labelTemplate = labelTemplate .replace(/\${(.*?)}/g, (match, attributeName) => { attributeName = attributeName.trim(); if (context?.feature?.properties?.[attributeName]) { let attributeValue = context.feature.properties[attributeName] || ""; if (typeof attributeValue !== "string") { attributeValue = String(attributeValue); } attributeValue = attributeValue.replace(/<br\s*\/?>/gi, "\n"); return attributeValue; } return ""; // Replace with empty if attribute not found }) .trim(); return labelTemplate; }, }; const layerStyle = { stroke: true, strokeColor: fileObj.color, strokeOpacity: fileObj.lineopacity, strokeWidth: fileObj.linesize, strokeDashstyle: fileObj.linestyle, fillColor: fileObj.color, fillOpacity: fileObj.fillOpacity, pointRadius: fileObj.fontsize, fontColor: fileObj.color, fontSize: fileObj.fontsize, labelOutlineColor: "black", labelOutlineWidth: fileObj.fontsize / 4, labelAlign: fileObj.labelpos, label: "${formatLabel}", }; const layerConfig = { styleContext: labelContext, styleRules: [ { predicate: () => true, style: layerStyle, }, ], }; let layerid = fileObj.filename.replace(/[^a-z0-9_-]/gi, "_"); // Using the SDK to add the layer with styles and zIndexing // Future Idea: Consider removing the return statements to test scenarios where two files with the same name load into the same layer but contain different features. // Potential Behavior: Only the second file might get saved to IndexedDB for reload purposes. This might need upstream handling in parserFile(). // Enhancement: Implement a prompt to ask users if they want to merge new file loads into existing layers. // Option: Provide a selection popup for users to choose which layer to merge if layers already exist. // Current Approach: For now, we stop execution and inform the user if the layer name is already in use. try { wmeSDK.Map.addLayer({ layerName: layerid, styleRules: layerConfig.styleRules, styleContext: layerConfig.styleContext, zIndexing: true, }); } catch (error) { if (error.name === "InvalidStateError") { console.error(`${scriptName}: Layer "${fileObj.filename}" already exists.`); WazeWrap.Alerts.error(scriptName, `Current Layer "${fileObj.filename}" already exists.`); return; } else { console.error(`${scriptName}: Unexpected error:`, error); WazeWrap.Alerts.error(scriptName, `Unexpected error creating Layer "${fileObj.filename}"`); return; } } // Set visibility to true for the layer wmeSDK.Map.setLayerVisibility({ layerName: layerid, visibility: true }); // Map features array with unique index-based IDs const featuresToLog = features.map((f, index) => ({ type: f.type, id: f.properties.OBJECTID || `${layerid}_${index}`, // Use feature index for uniqueness geometry: f.geometry, properties: f.properties, })); // Initialize counters let successCount = 0; let errorCount = 0; featuresToLog.forEach((feature) => { try { wmeSDK.Map.addFeatureToLayer({ feature: feature, layerName: layerid, }); successCount++; // Increment success counter } catch (error) { errorCount++; // Increment error counter if (error.name === "InvalidStateError") { console.error(`${scriptName}: Failed to add feature with ID: ${feature.id}. The layer "${layerid}" might not exist.`); } else if (error.name === "ValidationError") { console.error(`${scriptName}: Validation error for feature with ID: ${feature.id}. Check geometry type and properties.`, error); console.error(`${scriptName}: Feature details:`, feature); } else { console.error(`${scriptName}: Unexpected error adding feature with ID: ${feature.id}:`, error); console.error(`${scriptName}: Feature details:`, feature); } } }); // Log completion console.log(`${scriptName}: ${successCount} features added, ${errorCount} features skipped do to errors, for layer: ${fileObj.filename}`); // Add group toggler logic if necessary (assuming SDK supports it) if (!groupToggler) { groupToggler = addGroupToggler(false, "layer-switcher-group_wme_geometries", "WME Geometries"); } addToGeoList(fileObj.filename, fileObj.color, fileObj.orgFileext, fileObj.labelattribute, externalProjection); addLayerToggler(groupToggler, fileObj.filename, layerid); // Check and store layers in IndexedDB try { await storeLayer(fileObj); } catch (error) { console.error(`${scriptName}: Failed to store data in IndexedDB:`, error); WazeWrap.Alerts.error("Storage Error", "Failed to store data. Ensure IndexedDB is not full and try again. Layer will not be saved."); } } finally { toggleLoadingMessage(false); // Turn off the loading message! } }, delayDuration); } /**************************************************************************** * storeLayer * * Asynchronously stores a given file object in an IndexedDB object store named "layers". * If the file object is not already stored (determined by its filename), it will compress the object, * calculate its size in kilobits and megabits, and then store it. * * @param {Object} fileObj - The file object to be stored, which must include a 'filename' property. * * The function operates as follows: * 1. Checks whether the file identified by 'filename' already exists in the database. * 2. If the file does not exist: * a. Compresses the entire file object using LZString compression. * b. Calculates the size of the compressed data in bits, kilobits, and megabits. * c. Stores the compressed data with its filename in IndexedDB. * d. Logs a message to the console with the size details. * 3. If the file already exists, skips the storage process and logs a message. * * @returns {Promise} - Resolves when the file is successfully stored or skipped if it exists. * Rejects with an error if an operation fails. ****************************************************************************/ async function storeLayer(fileObj) { const transaction = db.transaction(["layers"], "readwrite"); const store = transaction.objectStore("layers"); return new Promise((resolve, reject) => { const request = store.get(fileObj.filename); request.onsuccess = function () { const existingLayer = request.result; if (!existingLayer) { // Compress the entire fileObj const compressedData = LZString.compress(JSON.stringify(fileObj)); // Calculate size of compressed data const byteSize = compressedData.length * 2; // Assuming 2 bytes per character const bitSize = byteSize * 8; const sizeInKilobits = bitSize / 1024; const sizeInMegabits = bitSize / 1048576; const compressedFileObj = { filename: fileObj.filename, // Keep the filename uncompressed compressedData, }; const addRequest = store.add(compressedFileObj); addRequest.onsuccess = function () { console.log(`${scriptName}: Stored Compressed Data file - ${fileObj.filename}. Size: ${sizeInKilobits.toFixed(2)} Kb, ${sizeInMegabits.toFixed(3)} Mb`); resolve(); }; addRequest.onerror = function (event) { console.error(`${scriptName}: Failed to store data in IndexedDB`, event.target.error); reject(new Error("Failed to store data")); }; } else { console.log(`${scriptName}: Skipping duplicate storage for file: ${fileObj.filename}`); resolve(); } }; request.onerror = function (event) { console.error(`${scriptName}: Failed to retrieve data from IndexedDB`, event.target.error); reject(new Error("Failed to retrieve data")); }; }); } function toggleLoadingMessage(show) { const existingMessage = document.getElementById("WMEGeoLoadingMessage"); if (show) { if (!existingMessage) { const loadingMessage = document.createElement("div"); loadingMessage.id = "WMEGeoLoadingMessage"; loadingMessage.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 16px 32px; background: rgba(0, 0, 0, 0.7); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); font-family: 'Arial', sans-serif; font-size: 1.1rem; text-align: center; z-index: 2000; color: #ffffff; border: 2px solid #ff5733;`; loadingMessage.textContent = "WME Geometries: New Geometries Loading, please wait..."; document.body.appendChild(loadingMessage); } } else { if (existingMessage) { existingMessage.remove(); } } } function toggleParsingMessage(show) { const existingMessage = document.getElementById("WMEGeoParsingMessage"); if (show) { if (!existingMessage) { const parsingMessage = document.createElement("div"); parsingMessage.id = "WMEGeoParsingMessage"; parsingMessage.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 16px 32px; background: rgba(0, 0, 0, 0.7); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); font-family: 'Arial', sans-serif; font-size: 1.1rem; text-align: center; z-index: 2000; color: #ffffff; border: 2px solid #33ff57;`; parsingMessage.textContent = "WME Geometries: Parsing and converting input files, please wait..."; document.body.appendChild(parsingMessage); } } else { if (existingMessage) { existingMessage.remove(); } } } /****************************************************************************** * Function: whatsInView * * Description: * Displays or updates a draggable overlay on the webpage, showing geographical data * currently in view. Calls `updateWhatsInView` to refresh content rather than rebuild * the overlay if it already exists. * * Main Operations: * - Checks for existing overlay (`WMEGeowhatsInViewMessage`). If present, updates it. * - Otherwise, creates new overlay elements styled for display. * - Makes the overlay draggable via its header. * - Integrates custom scrollbar styles for aesthetic purposes. * - Calls `updateWhatsInView` to fill the overlay with data. **********************************************************************************/ async function whatsInView() { let whatsInView = document.getElementById("WMEGeowhatsInViewMessage"); if (!whatsInView) { // Create the overlay if it doesn't exist whatsInView = document.createElement("div"); whatsInView.id = "WMEGeowhatsInViewMessage"; whatsInView.style = `position: absolute; padding: 0; background: rgba(0, 0, 0, 0.8); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); z-index: 1000; width: 375px; height: 375px; min-width: 200px; min-height: 200px; max-width: 30vw; max-height: 40vh; left: 50%; top: 50%; transform: translate(-50%, -50%); resize: both; overflow: hidden;`; const header = document.createElement("div"); header.style = `background: #33ff57; font-weight: 300; color: black; padding: 5px; border-radius: 12px 12px 0 0; display: flex; justify-content: space-between; align-items: center; height: 30px; position: sticky; top: 0; cursor: move;`; const title = document.createElement("span"); title.innerText = "WME Geo - Whats in View"; header.appendChild(title); const closeButton = document.createElement("span"); closeButton.textContent = "X"; closeButton.style = `cursor: pointer; font-size: 20px; margin-left: 10px;`; closeButton.addEventListener("click", () => { whatsInView.remove(); }); header.appendChild(closeButton); header.onmousedown = (event) => { event.preventDefault(); const initialX = event.clientX; const initialY = event.clientY; const offsetX = initialX - whatsInView.offsetLeft; const offsetY = initialY - whatsInView.offsetTop; document.onmousemove = (ev) => { whatsInView.style.left = `${ev.clientX - offsetX}px`; whatsInView.style.top = `${ev.clientY - offsetY}px`; }; document.onmouseup = () => { document.onmousemove = null; document.onmouseup = null; }; }; const contentContainer = document.createElement("div"); contentContainer.id = "WMEGeowhatsInViewContent"; contentContainer.style = `padding: 5px; height: calc(100% - 30px); overflow-y: auto; overflow-x: hidden; color: #ffffff; font-family: 'Arial', sans-serif; font-size: 1.0rem; text-align: left;`; whatsInView.appendChild(header); whatsInView.appendChild(contentContainer); const mapElement = document.getElementsByTagName("wz-page-content")[0]; if (mapElement) { mapElement.appendChild(whatsInView); } else { console.warn("DOM Element with the tag Name 'wz-page-content' not found."); //document.body.appendChild(whatsInView); } // Add custom scrollbar styles once const styleElement = document.createElement("style"); styleElement.innerHTML = `#WMEGeowhatsInViewMessage div::-webkit-scrollbar { width: 12px; } #WMEGeowhatsInViewMessage div::-webkit-scrollbar-track { background: #333333; border-radius: 10px; } #WMEGeowhatsInViewMessage div::-webkit-scrollbar-thumb { background-color: #33ff57; border-radius: 10px; border: 2px solid transparent; } #WMEGeowhatsInViewMessage div::-webkit-scrollbar-thumb:hover { background-color: #28d245; }`; document.head.appendChild(styleElement); } // Update content of the overlay (whether newly created or existing) await updateWhatsInView(whatsInView); //whatsInView JS55CT } /******************************************************************************** * Function: updateWhatsInView * * Description: * This asynchronous function populates the content of a specified overlay with * sorted geographical data, including states, counties, towns, and zip codes. * * Main Operations: * - Clears existing content in the overlay's content container. * - Fetches geographic data asynchronously for states, counties, towns, and zip codes. * - Organizes data hierarchically (states -> counties -> towns). * - Sorts each level of geographic entities alphabetically. * - Constructs HTML content to display sorted geographic information. * - Renders zip codes separately, sorted alphabetically. * * Parameters: * - whatsInView: The DOM element containing the overlay message to be updated. * * Returns: * - None * * Notes: * Ensures data is fetched and displayed in a structured and user-friendly format within the overlay. ******************************************************************************/ async function updateWhatsInView(whatsInView) { const contentContainer = whatsInView.querySelector("#WMEGeowhatsInViewContent"); if (!contentContainer) { console.error("Content container not found in existing message."); return; } contentContainer.innerHTML = ""; const dataTypes = ["state", "county", "countysub", "zipcode"]; const promises = dataTypes.map(async (dataType) => { try { return await getArcGISdata(dataType, false); } catch (error) { console.error(`Error fetching data for ${dataType}:`, error); return null; } }); const results = await Promise.all(promises); if (!results || results.length < 4 || !results[0] || !results[1] || !results[2] || !results[3]) { console.error("Failed to fetch necessary data"); return; } const [stateData, countyData, townData, zipData] = results; // Organize states const states = stateData.features.reduce((acc, feature) => { const stateName = feature.properties.NAME; const stateNum = feature.properties.STATE; acc[stateNum] = { name: stateName, counties: {} }; return acc; }, {}); // Organize counties under respective states countyData.features.forEach((feature) => { const countyName = feature.properties.NAME; const countyNum = feature.properties.COUNTY; const stateNum = feature.properties.STATE; if (states[stateNum]) { states[stateNum].counties[countyNum] = { name: countyName, towns: [] }; } }); // Organize towns under respective counties townData.features.forEach((feature) => { const townName = feature.properties.NAME; const countyNum = feature.properties.COUNTY; const stateNum = feature.properties.STATE; if (states[stateNum] && states[stateNum].counties[countyNum]) { states[stateNum].counties[countyNum].towns.push(townName); } }); let messageContent = ""; // Sort states by name before processing const sortedStates = Object.values(states).sort((a, b) => a.name.localeCompare(b.name)); sortedStates.forEach((state) => { messageContent += `<div style="margin-left: 0;"><strong>${state.name.toUpperCase()}:</strong></div>`; // Sort counties by name within each state const sortedCounties = Object.values(state.counties).sort((a, b) => a.name.localeCompare(b.name)); sortedCounties.forEach((county) => { messageContent += `<div style="margin-left: 20px;"><strong>${county.name}:</strong></div>`; // Sort towns by name within each county county.towns.sort().forEach((town) => { messageContent += `<div style="margin-left: 40px;">• ${town}</div>`; }); }); }); messageContent += `<br><strong>ZIP CODES:</strong><br>`; // Sort zip codes by name and add them to the content const sortedZipCodes = zipData.features.sort((a, b) => { const zipA = a.properties.BASENAME; const zipB = b.properties.BASENAME; return zipA.localeCompare(zipB); }); sortedZipCodes.forEach((feature) => { const zipName = feature.properties.BASENAME; messageContent += `<div style="margin-left: 20px;">• ${zipName}</div>`; }); contentContainer.innerHTML = messageContent; } /********************************************************************************************************** * presentFeaturesAttributesSDK * * Description: * Displays a user interface to facilitate the selection of an attribute from a set of geographic features. * If there is only one attribute, it automatically resolves with that attribute. Otherwise, it presents a modal * dialog with a dropdown list for the user to select the label attribute. * * Parameters: * @param {Array} features - An array of feature objects, each containing a set of attributes to choose from. * * Returns: * @returns {Promise} - A promise that resolves with the chosen attribute or rejects if the user cancels. * * Behavior: * - Immediately resolves if there is only one attribute across all features. * - Constructs a modal dialog centrally positioned on the screen to display feature properties. * - Iterates over the provided features, listing the attributes for each feature in a scrollable container. * - Utilizes a dropdown (`select` element) populated with the attributes for user selection. * - Includes "Import" and "Cancel" buttons to either resolve the promise with the selected attribute * or reject the promise, respectively. * - Ensures modal visibility with a semi-transparent overlay backdrop. *****************************************************************************************************/ function presentFeaturesAttributesSDK(features, nbFeatures) { return new Promise((resolve, reject) => { const allAttributes = features.map((feature) => Object.keys(feature.properties)); const attributes = Array.from(new Set(allAttributes.flat())); let attributeInput = document.createElement("div"); let title = document.createElement("label"); let propsContainer = document.createElement("div"); // Determine the theme to set appropriate styles const htmlElement = document.querySelector("html"); const theme = htmlElement.getAttribute("wz-theme") || "light"; if (theme === "dark") { attributeInput.style.cssText = "position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1001; width: 80%; max-width: 600px; padding: 10px; background: #333; border: 3px solid #666; border-radius: 5%; display: flex; flex-direction: column;"; title.style.cssText = "margin-bottom: 5px; color: #ddd; align-self: center; font-size: 1.2em;"; propsContainer.style.cssText = "overflow-y: auto; max-height: 300px; padding: 5px; border: 3px solid #444; border-radius: 10px; "; } else { attributeInput.style.cssText = "position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1001; width: 80%; max-width: 600px; padding: 10px; background:rgb(228, 227, 227); border: 3px solid #aaa; border-radius: 5%; display: flex; flex-direction: column;"; title.style.cssText = "margin-bottom: 5px; color: #333; align-self: center; font-size: 1.2em;"; propsContainer.style.cssText = "overflow-y: auto; max-height: 300px; padding: 5px; border: 2px solid black; border-radius: 10px;"; } title.innerHTML = `Feature Attributes<br>Total Features: ${nbFeatures}`; attributeInput.appendChild(title); let message = document.createElement("p"); message.style.cssText = "margin-top: 10px; color: #777; text-align: center;"; attributeInput.appendChild(propsContainer); features.forEach((feature, index) => { let featureHeader = document.createElement("label"); featureHeader.style.cssText = theme === "dark" ? "color: #ddd; font-size: 1.1em;" : "color: #333; font-size: 1.1em;"; featureHeader.textContent = `Feature ${index + 1}`; propsContainer.appendChild(featureHeader); let propsList = document.createElement("ul"); Object.keys(feature.properties).forEach((key) => { let propItem = document.createElement("li"); propItem.style.cssText = "list-style-type: none; padding: 2px; font-size: 0.9em;"; propItem.innerHTML = `<span style="color:rgb(53, 134, 187);">${key}</span>: ${feature.properties[key]}`; propsList.appendChild(propItem); }); propsContainer.appendChild(propsList); }); let inputLabel = document.createElement("label"); inputLabel.style.cssText = "display: block; margin-top: 15px;"; inputLabel.textContent = "Select Attribute to use for Label:"; attributeInput.appendChild(inputLabel); let selectBox = document.createElement("select"); selectBox.style.cssText = "width: 90%; padding: 8px; margin-top: 5px; margin-left: 5%; margin-right: 5%; border-radius: 5px;"; attributes.forEach((attribute) => { let option = document.createElement("option"); option.value = attribute; option.textContent = attribute; selectBox.appendChild(option); }); let noLabelsOption = document.createElement("option"); noLabelsOption.value = ""; noLabelsOption.textContent = "- No Labels -"; selectBox.appendChild(noLabelsOption); let customLabelOption = document.createElement("option"); customLabelOption.value = "custom"; customLabelOption.textContent = "Custom Label"; selectBox.appendChild(customLabelOption); attributeInput.appendChild(selectBox); // Dynamically apply inline styles to ensure precedence let customLabelInput = document.createElement("textarea"); customLabelInput.className = "custom-label-input"; customLabelInput.placeholder = `Enter your custom label using \${attributeName} for dynamic values. Feature 1 BridgeNumber: 01995 FacilityCarried: U.S. ROUTE 6 FeatureCrossed: BIG RIVER Example: (explicit new lines formatting) #:\${BridgeNumber}\\n\${FacilityCarried} over\\n\${FeatureCrossed} Example: (multi-line formatting) #:\${BridgeNumber} \${FacilityCarried} over \${FeatureCrossed} Expected Output: #:01995 U.S. ROUTE 6 over BIG RIVER`; customLabelInput.style.cssText = "color: #5DADE2 !important; width: 90%; height: 300px; max-height: 300px; padding: 8px; font-size: 1rem; border: 2px solid #ddd; border-radius: 5px; box-sizing: border-box; resize: vertical; display: none; margin-top: 5px; margin-left: 5%; margin-right: 5%;"; attributeInput.appendChild(customLabelInput); selectBox.addEventListener("change", () => { customLabelInput.style.display = selectBox.value === "custom" ? "block" : "none"; }); let buttonsContainer = document.createElement("div"); buttonsContainer.style.cssText = "margin-top: 10px; display: flex; justify-content: flex-end; width: 90%; margin-left: 5%; margin-right: 5%;"; let overlay = document.createElement("div"); overlay.id = "presentFeaturesAttributesOverlay"; overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;"; overlay.appendChild(attributeInput); let importButton = createButton("Import", "#8BC34A", "#689F38", "#FFFFFF", "button"); importButton.onclick = () => { if (selectBox.value === "custom" && customLabelInput.value.trim() === "") { WazeWrap.Alerts.error(scriptName, "Please enter a custom label expression when selecting 'Custom Label'."); return; } document.body.removeChild(overlay); let resolvedValue; if (selectBox.value === "custom" && customLabelInput.value.trim() !== "") { resolvedValue = customLabelInput.value.trim(); } else if (selectBox.value !== "- No Labels -") { resolvedValue = `\${${selectBox.value}}`; } else { resolvedValue = ""; } resolve(resolvedValue); }; let cancelButton = createButton("Cancel", "#E57373", "#D32F2F", "#FFFFFF", "button"); cancelButton.onclick = () => { document.body.removeChild(overlay); reject("Operation cancelled by the user"); }; buttonsContainer.appendChild(importButton); buttonsContainer.appendChild(cancelButton); attributeInput.appendChild(buttonsContainer); document.body.appendChild(overlay); }); } /************************************************************************************* * addToGeoList * * Description: * Adds a new list item (representing a geographic file) to the UI's geographic file list. Each item displays the filename * and includes a tooltip with additional file information, like file type, label attribute, and projection details. * A remove button is also provided to delete the layer from the list and handle associated cleanup. * * Parameters: * @param {string} filename - The name of the file, used as the display text and ID. * @param {string} color - The color used to style the filename text. * @param {string} fileext - The extension/type of the file; included in the tooltip. * @param {string} labelattribute - The label attribute used; included in the tooltip. * @param {Object} externalProjection - The projection details; included in the tooltip. * * Behavior: * - Creates a list item styled with CSS properties for layout and hover effects. * - Displays the filename in the specified color, with text overflow handling. * - Provides additional file details in a tooltip triggered on hover. * - Adds a remove button to each list item, invoking the `removeGeometryLayer` function when clicked. * - Appends each configured list item to the global `geolist` element for UI rendering. ****************************************************************************************/ function addToGeoList(filename, color, fileext, labelattribute, externalProjection) { let liObj = document.createElement("li"); liObj.id = filename.replace(/[^a-z0-9_-]/gi, "_"); liObj.style.cssText = "position: relative; padding: 2px 2px; margin: 2px 0; background: transparent; border-radius: 3px; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; font-size: 0.95em;"; liObj.addEventListener("mouseover", function () { liObj.style.background = "#eaeaea"; }); liObj.addEventListener("mouseout", function () { liObj.style.background = "transparent"; }); let fileText = document.createElement("span"); fileText.style.cssText = `color: ${color}; flex-grow: 1; flex-shrink: 1; flex-basis: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 5px;`; fileText.innerHTML = filename; const tooltipContent = `File Type: ${fileext}\nLabel: ${labelattribute}\nProjection: ${externalProjection}`; fileText.title = tooltipContent; liObj.appendChild(fileText); let removeButton = document.createElement("button"); removeButton.innerHTML = "X"; removeButton.style.cssText = "flex: none; background-color: #E57373; color: white; border: none; padding: 0; width: 16px; height: 16px; cursor: pointer; margin-left: 3px;"; removeButton.addEventListener("click", () => removeGeometryLayer(filename)); liObj.appendChild(removeButton); geolist.appendChild(liObj); } function createButton(text, bgColor, mouseoverColor, textColor, type = "button", labelFor = "") { let element; if (type === "label") { element = document.createElement("label"); element.textContent = text; if (labelFor) { element.htmlFor = labelFor; } } else if (type === "input") { element = document.createElement("input"); element.type = "button"; element.value = text; } else { element = document.createElement("button"); element.textContent = text; } element.style.cssText = `padding: 8px 0; font-size: 1.1rem; border: 2px solid ${bgColor}; border-radius: 20px; cursor: pointer; background-color: ${bgColor}; color: ${textColor}; box-sizing: border-box; transition: background-color 0.3s, border-color 0.3s; font-weight: bold; text-align: center; width: 95%; margin: 3px;`; // Apply !important to forcibly set color element.style.setProperty("color", textColor, "important"); element.addEventListener("mouseover", function () { element.style.backgroundColor = mouseoverColor; element.style.borderColor = mouseoverColor; }); element.addEventListener("mouseout", function () { element.style.backgroundColor = bgColor; element.style.borderColor = bgColor; }); return element; // Assuming you need to return the created element } /*************************************************************************** * removeGeometryLayer * * Description: * This function removes a specified geometry layer from the map, updates the stored layers, * and manages corresponding UI elements and local storage entries. * * Parameters: * @param {string} filename - The name of the file associated with the geometry layer to be removed. * * Behavior: * - Identifies and destroys the specified geometry layer from the map. * - Updates the `storedLayers` array by removing the layer corresponding to the filename. * - Identifies and removes UI elements associated with the layer: * - The toggler item identified by the prefixed ID "t_[sanitizedFilename]". * - The list item identified by the filename. * - Updates local storage: * - Removes the entire storage entry if no layers remain. * - Compresses and updates the storage entry with remaining layers if any exist. * - Logs the changes in local storage size. ****************************************************************************/ async function removeGeometryLayer(filename) { const layerName = filename.replace(/[^a-z0-9_-]/gi, "_"); try { // Use the SDK to remove the layer wmeSDK.Map.removeLayer({ layerName: layerName }); console.log(`${scriptName}: Layer removed with ID: ${layerName}`); // Asynchronously remove the layer from IndexedDB try { await removeLayerFromIndexedDB(filename); console.log(`${scriptName}: Removed file - ${filename} from IndexedDB.`); } catch (error) { console.error(`${scriptName}: Failed to remove layer ${filename} from IndexedDB:`, error); } // Sanitize filename and define IDs const listItemId = filename.replace(/[^a-z0-9_-]/gi, "_"); const layerTogglerId = `t_${listItemId}`; // Remove the toggler item if it exists const togglerItem = document.getElementById(layerTogglerId); if (togglerItem?.parentElement) { togglerItem.parentElement.removeChild(togglerItem); } // Remove any list item using the listItemId const listItem = document.getElementById(listItemId); if (listItem) { listItem.remove(); } } catch (error) { console.error(`${scriptName}: Failed to remove layer or UI elements for ${filename}:`, error); } } // Function to remove a layer from IndexedDB async function removeLayerFromIndexedDB(filename) { if (!db) { // Check if the database is initialized return Promise.reject(new Error("Database not initialized")); } return new Promise((resolve, reject) => { const transaction = db.transaction(["layers"], "readwrite"); const store = transaction.objectStore("layers"); const request = store.delete(filename); // Transaction-level error handling transaction.onerror = function (event) { reject(new Error("Transaction failed during deletion")); }; // Request-specific success and error handling request.onsuccess = function () { resolve(); }; request.onerror = function (event) { reject(new Error("Failed to delete layer from database")); }; }); } /******************************************************************** * createLayersFormats * * Description: * Initializes and returns an object of supported geometric data formats for use in parsing and rendering map data. * This function checks for the availability of various format parsers and creates instance objects for each, capturing * any parsing capabilities with error handling. * * Process: * - Verifies the availability of the `Wkt` library, logging an error if it is not loaded. * - Defines a helper function `tryCreateFormat` to instantiate format objects and log successes or errors. * - Attempts to create format instances for GEOJSON, WKT, KML, GPX, GML, OSM, and ZIP files of (SHP,SHX,DBF) using corresponding constructors. * - Continually builds a `formathelp` string that lists all formats successfully instantiated. * - Returns an object containing both the instantiated formats and the help string. * * Notes: * - Ensures debug logs are provided for tracing function execution when `debug` mode is active. **************************************************************/ function createLayersFormats() { const formats = {}; let formathelp = ""; function tryCreateFormat(formatName, formatUtility) { try { if (typeof formatUtility === "function") { try { const formatInstance = new formatUtility(); formats[formatName] = formatInstance; } catch (constructorError) { formats[formatName] = formatUtility; } formathelp += `${formatName} | `; console.log(`${scriptName}: Successfully added format: ${formatName}`); } else if (formatUtility) { formats[formatName] = formatUtility; formathelp += `${formatName} | `; console.log(`${scriptName}: Successfully added format: ${formatName}`); } else { console.warn(`${scriptName}: ${formatName} is not a valid function or instance.`); } } catch (error) { console.error(`${scriptName}: Error creating format ${formatName}:`, error); } } // Add GEOJSON format formats["GEOJSON"] = "GEOJSON"; formathelp += "GEOJSON | "; console.log(`${scriptName}: Successfully added format: GEOJSON`); // Add other formats using custom parsers or utilities tryCreateFormat("KML", typeof GeoKMLer !== "undefined" && GeoKMLer); tryCreateFormat("KMZ", typeof GeoKMZer !== "undefined" && GeoKMZer); tryCreateFormat("GML", typeof GeoGMLer !== "undefined" && GeoGMLer); tryCreateFormat("GPX", typeof GeoGPXer !== "undefined" && GeoGPXer); tryCreateFormat("WKT", typeof GeoWKTer !== "undefined" && GeoWKTer); if (typeof GeoSHPer !== "undefined") { formats["ZIP"] = GeoSHPer; formathelp += "ZIP(SHP,DBF,PRJ,CPG) | "; console.log(`${scriptName}: Successfully added format: ZIP (shapefile)`); } else { console.error(`${scriptName}: Shapefile support (GeoSHPer) is not available.`); } console.log(`${scriptName}: Finished loading document format parsers.`); return { formats, formathelp }; } /*********************************************************************************** * addGroupToggler * * Description: * This function creates and adds a group toggler to a layer switcher UI component. It manages the visibility and interaction * of different layer groups within a map or similar UI, providing a toggling mechanism for user interface groups. * * Parameters: * @param {boolean} isDefault - A flag indicating whether the group is a default group. * @param {string} layerSwitcherGroupItemName - The unique name used as an identifier for the layer switcher group element. * @param {string} layerGroupVisibleName - The human-readable name for the layer group, shown in the UI. * * Returns: * @returns {HTMLElement} - The group element that has been created or modified. * * Behavior: * - If `isDefault` is true, it retrieves and operates on the existing group element related to the provided name. * - Otherwise, it dynamically creates a new group list item. * - Builds a toggler that includes a caret icon, a toggle switch, and a label displaying the group's visible name. * - Attaches event handlers to manage collapsible behavior of the group toggler and switches. * - Appends the configured group to the main UI component, either as an existing group or newly created one. * - Logs the creation of the group toggler to the console for debugging purposes. *****************************************************************************************/ function addGroupToggler(isDefault, layerSwitcherGroupItemName, layerGroupVisibleName) { var group; if (isDefault === true) { group = document.getElementById(layerSwitcherGroupItemName).parentElement.parentElement; } else { var layerGroupsList = document.getElementsByClassName("list-unstyled togglers")[0]; group = document.createElement("li"); group.className = "group"; var togglerContainer = document.createElement("div"); togglerContainer.className = "layer-switcher-toggler-tree-category"; var groupButton = document.createElement("wz-button"); groupButton.color = "clear-icon"; groupButton.size = "xs"; var iCaretDown = document.createElement("i"); iCaretDown.className = "toggle-category w-icon w-icon-caret-down"; iCaretDown.dataset.groupId = layerSwitcherGroupItemName.replace("layer-switcher-", "").toUpperCase(); var togglerSwitch = document.createElement("wz-toggle-switch"); togglerSwitch.className = layerSwitcherGroupItemName + " hydrated"; togglerSwitch.id = layerSwitcherGroupItemName; togglerSwitch.checked = true; var label = document.createElement("label"); label.className = "label-text"; label.htmlFor = togglerSwitch.id; var togglerChildrenList = document.createElement("ul"); togglerChildrenList.className = "collapsible-" + layerSwitcherGroupItemName.replace("layer-switcher-", "").toUpperCase(); label.appendChild(document.createTextNode(layerGroupVisibleName)); groupButton.addEventListener("click", layerTogglerGroupMinimizerEventHandler(iCaretDown)); togglerSwitch.addEventListener("click", layerTogglerGroupMinimizerEventHandler(iCaretDown)); groupButton.appendChild(iCaretDown); togglerContainer.appendChild(groupButton); togglerContainer.appendChild(togglerSwitch); togglerContainer.appendChild(label); group.appendChild(togglerContainer); group.appendChild(togglerChildrenList); layerGroupsList.appendChild(group); } if (debug) console.log(`${scriptName}: Layer Group Toggler created for ${layerGroupVisibleName}`); return group; } /****************************************************************************** * addLayerToggler * * Description: * This function adds a toggler for individual layers within a group in a layer switcher UI component. It manages the visibility * and interaction for specific map layers, allowing users to toggle them on and off within a UI group. * * Parameters: * @param {HTMLElement} groupToggler - The parent group toggler element under which the layer toggler is added. * @param {string} layerName - The name of the layer, used for display and creating unique identifiers. * @param {Object} layerObj - The layer object that is being toggled, typically representing a map or UI layer. * * Behavior: * - Locates the container (UL) within the group toggler where new layer togglers are to be appended. * - Creates a checkbox element for the layer, setting it to checked by default for visibility. * - Attaches events to both the individual layer checkbox and the group checkbox for toggling functionality. * - Appends the fully configured toggler to the UI. * - Logs the creation of the layer toggler for debugging purposes. *****************************************************************************/ function addLayerToggler(groupToggler, layerName, layerId) { const layer_container = groupToggler.getElementsByTagName("UL")[0]; const layerGroupCheckbox = groupToggler.getElementsByClassName("layer-switcher-toggler-tree-category")[0].getElementsByTagName("wz-toggle-switch")[0]; const toggler = document.createElement("li"); const togglerCheckbox = document.createElement("wz-checkbox"); togglerCheckbox.setAttribute("checked", "true"); // Generate ID for togglerCheckbox using layerName const togglerId = "t_" + layerId; togglerCheckbox.id = togglerId; togglerCheckbox.className = "hydrated"; togglerCheckbox.appendChild(document.createTextNode(layerName)); toggler.appendChild(togglerCheckbox); layer_container.appendChild(toggler); // Attach event handlers using layerId to manage visibility with SDK togglerCheckbox.addEventListener("change", layerTogglerEventHandler(layerId)); layerGroupCheckbox.addEventListener("change", layerTogglerGroupEventHandler(togglerCheckbox, layerId)); if (debug) console.log(`${scriptName}: Layer Toggler created for ${layerName}`); } function layerTogglerEventHandler(layerId) { return function () { const isVisible = this.checked; try { wmeSDK.Map.setLayerVisibility({ layerName: layerId, visibility: isVisible, }); } catch (error) { console.error(`Failed to set visibility for layer with ID ${layerId}:`, error); } if (debug) console.log(`${scriptName}: Layer visibility set to ${isVisible} for layer ${layerId}`); }; } function layerTogglerGroupEventHandler(groupCheckbox, layerId) { return function () { const shouldBeVisible = this.checked && groupCheckbox.checked; try { wmeSDK.Map.setLayerVisibility({ layerName: layerId, visibility: shouldBeVisible, }); } catch (error) { console.error(`Failed to set group visibility for layer ${layerId}:`, error); } groupCheckbox.disabled = !this.checked; if (!groupCheckbox.checked) { groupCheckbox.disabled = false; } if (debug) console.log(`${scriptName}: WME Geometries Group Layer visibility set to ${shouldBeVisible}`); }; } function layerTogglerGroupMinimizerEventHandler(iCaretDown) { return function () { const ulCollapsible = iCaretDown.closest("li").querySelector("ul"); iCaretDown.classList.toggle("upside-down"); ulCollapsible.classList.toggle("collapse-layer-switcher-group"); }; } /**************************************************************************************** * getArcGISdata * * Fetches geographic data from an ArcGIS service based on specified data types and options. * This function constructs a query URL to retrieve data from the specified ArcGIS endpoint, * obtaining either point or extent geometry based on the current view of the map within WME (Waze Map Editor). * * Parameters: * @param {string} [dataType="state"] - The type of geographic data to fetch, with possible values * such as "state", "county", "countysub", "zipcode". Each data type corresponds to a different endpoint. * @param {boolean} [returnGeo=true] - Indicates whether the function should include geometry in the response. * If true, the geometry is included; if false, the function queries the current extent for visible regions. * * Returns: * @returns {Promise<Object>} - A promise that resolves to a JSON object containing the requested geographic data. * The data is formatted as GeoJSON with added CRS (Coordinate Reference System) information. * * Workflow: * - Validates the dataType argument to ensure it matches a predefined configuration. * - Depending on returnGeo, sets the geometry parameter to either a center point or map extent. * - Constructs a query string for the ArcGIS REST API, including necessary spatial information. * - Initiates an HTTP GET request using GM_xmlhttpRequest, handling success and error responses. * - Parses the GeoJSON response and attaches CRS information. * * Errors: * - Throws an error for invalid dataType options. * - Rejects the promise if JSON parsing fails or the HTTP request encounters an error. ****************************************************************************************/ function getArcGISdata(dataType = "state", returnGeo = true) { // Define URLs and field names for each data type const CONFIG = { state: { url: "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/State_County/MapServer/0", outFields: "BASENAME,NAME,STATE", }, county: { url: "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/State_County/MapServer/1", outFields: "BASENAME,NAME,STATE,COUNTY", }, countysub: { url: "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Places_CouSub_ConCity_SubMCD/MapServer/1", outFields: "BASENAME,NAME,STATE,COUNTY", }, zipcode: { url: "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/PUMA_TAD_TAZ_UGA_ZCTA/MapServer/1", outFields: "BASENAME", }, // Add more configurations as needed }; // Check if the dataType is valid const config = CONFIG[dataType.toLowerCase()]; if (!config) { throw new Error(`Invalid data type: ${dataType}`); } let geometry; let geometryType; if (returnGeo) { // Obtain the center of the map in WGS84 format and Create a geometry object for it const wgs84Center = wmeSDK.Map.getMapCenter(); // Get the current center coordinates of the WME map geometry = { x: wgs84Center.lon, y: wgs84Center.lat, spatialReference: { wkid: 4326 }, }; geometryType = "esriGeometryPoint"; } else { // Get current map extent and visible regions const wgs84Extent = wmeSDK.Map.getMapExtent(); geometry = { xmin: wgs84Extent[0], ymin: wgs84Extent[1], xmax: wgs84Extent[2], ymax: wgs84Extent[3], spatialReference: { wkid: 4326 }, }; geometryType = "esriGeometryEnvelope"; } const url = `${config.url}/query?geometry=${encodeURIComponent(JSON.stringify(geometry))}`; const queryString = `${url}&outFields=${encodeURIComponent(config.outFields)}&returnGeometry=${returnGeo}&spatialRel=esriSpatialRelIntersects` + `&geometryType=${geometryType}&inSR=${geometry.spatialReference.wkid}&outSR=${geometry.spatialReference.wkid}&f=GeoJSON`; if (debug) console.log(`${scriptName}: getArcGISdata(${dataType})`, queryString); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: queryString, method: "GET", onload: function (response) { try { const jsonResponse = JSON.parse(response.responseText); // Add CRS information to the GeoJSON response jsonResponse.crs = { type: "name", properties: { name: "EPSG:4326", }, }; resolve(jsonResponse); // Resolve the promise with the JSON response } catch (error) { reject(new Error("Failed to parse JSON response: " + error.message)); } }, onerror: function (error) { reject(new Error("Request failed: " + error.statusText)); }, }); }); } }; geometries();