GeoWKTer is a JavaScript library designed to convert Well-Known Text (WKT) representations of geometries into GeoJSON format. This tool is useful for developers and GIS specialists who need to work with geographic data across different standards.
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/523986/1537670/GeoWKTer.js
// ==UserScript== // @name GeoWKTer // @namespace https://github.com/JS55CT // @description geoWKTer is a JavaScript library designed to convert WKT data into GeoJSON format efficiently. It supports conversion of Point, LineString, Polygon, and MultiGeometry elements. // @version 2.1.0 // @author JS55CT // @license MIT // @match *://this-library-is-not-supposed-to-run.com/* // ==/UserScript== /*************************************** * GeoWKTer Constructor Function * The `GeoWKTer` function serves as a constructor for creating instances that * manage the conversion of Well-Known Text (WKT) strings into GeoJSON representations. * It initializes regex patterns used for parsing WKT, and offers a structure to hold * spatial features processed from those strings. * * MIT License * Copyright (c) 2022 hu de yi * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * GeoWKTer inspired by the work of Wicket.js <https://github.com/arthur-e/Wicket> (GPL-3.0 licensed) * and terraformer <https://github.com/terraformer-js/terraformer/tree/main> (MIT licensed) ****************************************/ var GeoWKTer = (function () { function GeoWKTer() { this.features = []; // Initialize an array to store parsed feature data this.regExes = { // Regular expressions for parsing WKT strings typeStr: /^\s*(\w+)\s*\((.*)\)\s*$/, // Capture geometry type and contents spaces: /\s+|\+/, // Detect spaces or plus signs for splitting coordinates comma: /\s*,\s*/, // Capture commas surrounded by optional whitespace parenComma: /\)\s*,\s*\(/, // Split on closing parentheses followed by a comma and opening parentheses }; } /***************************************** * Clean WKT String * * This function processes a Well-Known Text (WKT) string, * removing unnecessary spaces and newlines within * parenthesized coordinate lists. It ensures commas have * consistent spacing, typically immediately following * coordinate values. Primarily targets complex geometries * like MULTIPOLYGON, optimizing coordinate definition * clarity and compactness. * * @param {string} wkt - The WKT string to clean. * @returns {string} - The cleaned WKT string, with optimized spacing. *****************************************/ GeoWKTer.prototype.cleanWKTString = function (wkt) { return wkt .replace(/[\n\r]+/g, " ") // Replace newlines with a single space .replace(/\s\s+/g, " ") // Replace multiple spaces with a single space .replace(/([A-Z]+)\s*\(/g, "$1(") // Ensure no space between type and opening parenthesis .replace(/\(\s+/g, "(") // Remove spaces after opening parentheses .replace(/\s+\)/g, ")") // Remove spaces before closing parentheses .replace(/,\s+/g, ",") // Remove spaces after commas .replace(/\s+,/g, ",") // Remove spaces before commas .trim(); // Trim leading and trailing whitespaces }; /*************************************** * Read WKT and Convert to Internal Representation * This function takes a Well-Known Text (WKT) string, processes it to produce * a cleaned and standardized version, and then converts it to an internal data * structure that represents the geometry for further processing or transformation * to GeoJSON. This step is crucial for understanding and manipulating spatial data. * * @param {string} wktText - The original WKT string representing a geometry or * collection of geometries (e.g., POINT, POLYGON). * @param {string} label - A descriptive label or identifier associated with the * geometry, which will be stored for use in GeoJSON properties. * @returns {Object[]} - An array containing a single object with: * - type: the type of geometry (e.g., POINT, POLYGON). * - components: the coordinates or geometries depending on type. * - label: the provided label for this geometry. * @throws {Error} - Throws an error if the WKT is malformed or cannot be processed, * indicating the WKT string is invalid or unsupported. * * Procedure: * 1. Clean WKT String: Utilize `cleanWKTString` to standardize the WKT input, * handling cases like whitespace normalization, and ensuring it matches expected format. * * 2. Convert to GeoJSON: Pass the cleaned WKT string to `wktToGeoJSON` for transformation * into a GeoJSON-like structure. This process involves parsing the WKT syntax into * an object with a `type` and relevant coordinates or geometries attribute. * * 3. Construct Internal Representation: Return the parsed structure as a new object with: * - `type`: Captured from the GeoJSON object. * - `components`: The coordinates (or geometries array) reflecting the parsed data. * - `label`: Passed through for later use in properties or identifications. * * 4. Error Handling: Any failure in parsing triggers an exception with a descriptive message, * alerting the user or developer to malformed or unsupported inputs. ****************************************/ GeoWKTer.prototype.read = function (wktText, label) { try { // Clean and standardize the input WKT string const cleanedWKT = this.cleanWKTString(wktText); // Convert the cleaned WKT to a GeoJSON-like structure const geoJSON = this.wktToGeoJSON(cleanedWKT); // Return the internal representation with the given label return [ { type: geoJSON.type, // Extract the geometry type components: geoJSON.coordinates || geoJSON.geometries, // Choose coordinates or geometries attribute based on type label, // Add the provided label for future reference }, ]; } catch (error) { // Handle and throw errors related to malformed or unsupported WKT throw new Error(error.message); } }; /*************************************** * Convert Internal Data Array to GeoJSON * This function takes an array of internal data objects—each representing * parsed WKT geometries—and converts them into a valid GeoJSON object. * Specifically, it structures these geometries into GeoJSON features within * a FeatureCollection, appropriately handling both individual geometries * and collections of geometries. * * @param {Object[]} dataArray - An array of objects where each contains: * - type: the geometric type (e.g., POINT, POLYGON). * - components: either the coordinates of a * single geometry or an array of geometries. * - label: optional description used as a property. * @returns {Object} - GeoJSON object formatted as a FeatureCollection, comprising * individual features with their associated geometries and properties. * * Steps Involved: * 1. Initialize `features`: Accumulate each processed geometry into this array * as a GeoJSON `Feature`, maintaining a list to be included in the final * `FeatureCollection`. * * 2. Iterate Over Data Array: For each geometry object from the parsed internal * data: * * - **Geometry Collections**: * - Identify the `GEOMETRYCOLLECTION` type and ensure `components` is an array. * - Iterate through each geometry in the collection, converting each into * a GeoJSON `Feature` by specifying its type and coordinates, and pushing * it to the `features` list. * * - **Other Geometries**: * - Construct a GeoJSON `Feature` using the individual geometry's `type` and * `coordinates` directly. * - Append each to `features`, ensuring they include property details. * * 3. Construct FeatureCollection: Wrap the accumulated features array into a * GeoJSON formatted object by designating it as a `FeatureCollection`. * * WARNING: The input WKT geometries do not include spatial reference system (SRS) information. * This function purely reformats the WKT to GeoJSON without assuming or verifying any particular * coordinate reference system. If the input geometries are not in the standard EPSG:4326 (WGS 84), * the resulting GeoJSON will also lack standard CRS information, which may lead to incorrect spatial * data representation or interpretation. * Users should ensure that the input data is in the intended coordinate system for their applications. ****************************************/ GeoWKTer.prototype.toGeoJSON = function (dataArray) { // Reduce the internal data array into a GeoJSON features array const features = dataArray.reduce((accum, data) => { const { type, components, label } = data; // Destructure for ease of use if (type === "GEOMETRYCOLLECTION" && Array.isArray(components)) { // If it's a geometry collection, iterate over its components components.forEach((geometry) => { accum.push({ // Push each as a Feature to the GeoJSON features list type: "Feature", geometry: { // Define the geometry object for GeoJSON type: geometry.type, coordinates: geometry.coordinates, }, properties: { // Attach properties, including label if provided Name: label || "", }, }); }); } else { // Handle non-collection geometries directly as a single GeoJSON feature accum.push({ type: "Feature", geometry: { // Assign geometry details type: type, coordinates: components, }, properties: { Name: label || "", // Include label as a property if available }, }); } return accum; // Return the accumulator for the next iteration }, []); // Return the complete GeoJSON FeatureCollection return { type: "FeatureCollection", features: features, // Embed the compiled features }; }; /*************************************** * Convert WKT to GeoJSON * @param {string} wkt - The WKT string. * @returns {Object} - GeoJSON object. * @throws {Error} - Throws if WKT is unsupported or invalid. ****************************************/ GeoWKTer.prototype.wktToGeoJSON = function (wkt) { const match = this.regExes.typeStr.exec(wkt); if (!match) throw new Error("Invalid WKT"); const type = match[1].toUpperCase(); const data = match[2]; const parsers = { POINT: this.parsePoint, LINESTRING: this.parseLineString, POLYGON: this.parsePolygon, MULTIPOINT: this.parseMultiPoint, MULTILINESTRING: this.parseMultiLineString, MULTIPOLYGON: this.parseMultiPolygon, GEOMETRYCOLLECTION: this.parseGeometryCollection, }; if (!parsers[type]) { throw new Error(`Unsupported WKT type: ${type}`); } const result = parsers[type].call(this, data); if (type === "GEOMETRYCOLLECTION") { return { type, geometries: result }; } return { type, coordinates: result }; }; /*************************************** * Parse Point Geometry * @param {string} str - The WKT coordinates string. * @returns {number[]} - Array of numbers representing the point. ****************************************/ GeoWKTer.prototype.parsePoint = function (str) { return str.trim().split(" ").map(Number); }; /*************************************** * Parse LineString Geometry * @param {string} str - The WKT coordinates string. * @returns {number[][]} - Array of arrays representing the linestring. ****************************************/ GeoWKTer.prototype.parseLineString = function (str) { return str.split(",").map((pair) => { return pair.trim().split(" ").map(Number); }); }; /*************************************** * Parse Polygon Geometry * @param {string} str - The WKT coordinates string. * @returns {number[][][]} - Array of arrays representing the polygon. ****************************************/ GeoWKTer.prototype.parsePolygon = function (str) { return str.match(/\([^()]+\)/g).map((ring) => { return ring .replace(/[()]/g, "") .split(",") .map((pair) => { return pair.trim().split(" ").map(Number); }); }); }; /*************************************** * Parse MultiPoint Geometry * @param {string} str - The WKT coordinates string. * @returns {number[][]} - Array of points representing the multipoint. ****************************************/ GeoWKTer.prototype.parseMultiPoint = function (str) { // If the WKT includes nested parentheses const matchParenPoints = str.match(/\(\s*([^()]+)\s*\)/g); if (matchParenPoints) { return matchParenPoints.map((pointStr) => { return this.parsePoint(pointStr.replace(/[()]/g, "").trim()); }); } else { return str.split(",").map(this.parsePoint.bind(this)); } }; /*************************************** * Parse MultiLineString Geometry * @param {string} str - The WKT coordinates string. * @returns {number[][][]} - Array of linestrings representing the multilinestring. ****************************************/ GeoWKTer.prototype.parseMultiLineString = function (str) { return str.match(/\(([^()]+)\)/g).map((linestring) => { return this.parseLineString(linestring.replace(/[()]/g, "").trim()); }); }; /*************************************** * Parse MultiPolygon Geometry * @param {string} str - The WKT coordinates string. * @returns {number[][][][]} - Array of polygons representing the multipolygon. ****************************************/ GeoWKTer.prototype.parseMultiPolygon = function (str) { // Match groups of polygons within MULTIPOLYGON const polygonMatches = str.match(/\(\([^)]+\)\)/g); if (!polygonMatches) { throw new Error("Invalid MULTIPOLYGON WKT format"); } return polygonMatches.map((polygonStr) => { // Each match represents a polygon, stripping the outer parentheses const cleanedPolygonStr = polygonStr.slice(1, -1); // Removes the outermost two parenthesis levels return this.parsePolygon(cleanedPolygonStr); }); }; /*************************************** * Extract Geometries from WKT GeometryCollection * This function scans through a WKT formatted string that represents a * GEOMETRYCOLLECTION and identifies individual geometry components within * it. The function splits the string into manageable parts, each corresponding * to a distinct geometry (e.g., POINT, LINESTRING). * * @param {string} str - The WKT string containing the collection of geometries. * @returns {string[]} - An array of WKT strings, each representing a * single geometry component from the GEOMETRYCOLLECTION. * * Procedure: * 1. Initialize an array `geometries` to collect extracted WKT segments. * 2. Define `geometryTypes`, an array containing the WKT keywords for supported * geometry types, which are POINT, LINESTRING, POLYGON, MULTIPOINT, * MULTILINESTRING, and MULTIPOLYGON. * 3. Utilize two variables, `depth` and `start`, to track the nesting level * of parentheses and the start position of each geometry component. * * Main Loop: * - Iterate over each character in the string. * - Adjust `depth` to reflect the current level of parenthesis nesting, * incrementing with '(' and decrementing with ')'. * - Upon reaching `depth` 0, examine the current location in the string * for potential geometry type keywords. * - When a geometry type is detected at `depth` 0, mark the end of the * preceding geometry segment, if any, and append it to `geometries`. * - Set `start` to the current index, marking the beginning of the next geometry. * * Finalization: * - After exiting the loop, capture any remaining geometry from `start` to * the end of the string, appending it to the `geometries` list. * * This approach ensures each nested geometry in a GEOMETRYCOLLECTION is * accurately isolated as its own distinct WKT segment, ready for parsing. ****************************************/ GeoWKTer.prototype.extractGeometries = function (str) { const geometries = []; // Array to store each extracted geometry WKT const geometryTypes = ["POINT", "LINESTRING", "POLYGON", "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON"]; // Known geometry types let depth = 0; // Tracks current depth level of parentheses let start = 0; // Marks the start index of the current geometry segment // Iterate over each character in the WKT string for (let i = 0; i < str.length; i++) { if (str[i] === "(") depth++; // Increment depth for each opening parenthesis if (str[i] === ")") depth--; // Decrement depth for each closing parenthesis // Check if we're at a zero depth, potentially between geometries if (depth === 0) { // Explore possible starts of a new geometry type geometryTypes.forEach((type) => { if (str.startsWith(type, i)) { // Check if this point indicates a new geometry type if (i > start) { // Ensure there's a segment before this new start const geometry = str.slice(start, i).trim(); // Extract the previous geometry segment if (geometry) { // Ensure it's non-empty geometries.push(geometry); // Add to the list of extracted geometries } } start = i; // Update start to the current position for the new geometry } }); } } // Process the final segment if there's any left if (start < str.length) { const geometry = str.slice(start).trim(); // Extract remaining geometry segment if (geometry) { // Ensure non-empty geometries.push(geometry); // Add last geometry } } return geometries; // Return all extracted geometry components }; /*************************************** * Parse GeometryCollection * This function processes a Well-Known Text (WKT) input string that represents * a GEOMETRYCOLLECTION, and converts it into an array of geometry objects * (not features at this stage) with identifying characteristics to be * transformed into GeoJSON later. * * @param {string} str - The WKT string for the geometry collection, usually in the form * 'GEOMETRYCOLLECTION(POINT(...), LINESTRING(...), ...)'. * @param {string} [label] - An optional label for each geometry parsed, which may be used * later as a name property in GeoJSON. * @returns {Object[]} - An array of geometry objects, each containing: * - type: the type of geometry (e.g., POINT, LINESTRING). * - coordinates: numerical array(s) representing the geometry's * spatial data. * * The following steps are performed: * 1. Setting up parsers: Retrieve the appropriate parsing functions for each * known geometry type, such as POINT or POLYGON, from a predefined map of * methods. * * 2. Extract geometries: Invoke `extractGeometries` to decompose the WKT string * into its respective geometry components based on delimiters and nesting, * isolating each geometry's WKT sub-string. * * 3. Parse each geometry: Iterate through the extracted geometries: * - Use regular expressions to identify the type and retrieve the coordinate details. * - Apply the corresponding parser to transform WKT coordinates into numerical arrays. * - Store the result as an object with `type` and `coordinates` attributes. * * 4. Error Handling: If a geometry type is unsupported or if parsing fails, an * exception is raised to indicate incorrect or unsupported formatting. * * This function does not wrap the geometries in GeoJSON features but prepares * the data in a way that can be easily transformed into GeoJSON format by * following processes. ****************************************/ GeoWKTer.prototype.parseGeometryCollection = function (str, label = "") { const components = []; // Array to hold parsed geometry objects // Map for geometry type to the appropriate parsing function const parsers = { POINT: this.parsePoint, LINESTRING: this.parseLineString, POLYGON: this.parsePolygon, MULTIPOINT: this.parseMultiPoint, MULTILINESTRING: this.parseMultiLineString, MULTIPOLYGON: this.parseMultiPolygon, }; // Extract each geometry from the GeometryCollection WKT string const geometries = this.extractGeometries(str); // Process each individual WKT geometry geometries.forEach((geometryWKT) => { // Match the geometry type and coordinate section const match = geometryWKT.match(/([A-Z]+)\s*\((.*)\)/i); if (match) { const type = match[1].toUpperCase(); // Capture geometry type const parser = parsers[type]; // Get parser for this geometry type if (parser) { // Parse coordinates using the relevant parser function const coordinates = parser.call(this, match[2].trim()); components.push({ type: type, // Store geometry type coordinates: coordinates, // Store parsed coordinates }); } else { // Raise error if unsupported geometry type is encountered throw new Error(`Unsupported geometry type: ${type}`); } } else { // Raise error if WKT parsing fails throw new Error("Failed to parse geometry WKT"); } }); return components; // Return the array of parsed geometry objects }; return GeoWKTer; })();