// ==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();