// ==UserScript==
// @name GeoKMLer
// @namespace https://github.com/JS55CT
// @description geoKMLer is a JavaScript library designed to convert KML data into GeoJSON format efficiently. It supports conversion of Placemarks containing Point, LineString, Polygon, and MultiGeometry elements.
// @version 2.2.0
// @author JS55CT
// @license MIT
// @match *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==
/***********************************************************
* ## Project Home < https://github.com/JS55CT/GeoKMLer >
* MIT License
* Copyright (c) 2025 Justin
* 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.
**************************************************************/
var GeoKMLer = (function () {
/**
* GeoKMLer constructor function.
* @param {Object} obj - Optional object to wrap.
* @returns {GeoKMLer} - An instance of GeoKMLer.
*/
function GeoKMLer(obj) {
if (obj instanceof GeoKMLer) return obj;
if (!(this instanceof GeoKMLer)) return new GeoKMLer(obj);
this._wrapped = obj;
}
/**
* Parses a KML string into an XML DOM.
* @param {string} kmlText - The KML text to parse.
* @returns {Document} - The parsed XML document.
*/
GeoKMLer.prototype.read = function (kmlText) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(kmlText, "application/xml");
// Check for parsing errors by looking for parser error tags
const parseErrors = xmlDoc.getElementsByTagName("parsererror");
if (parseErrors.length > 0) {
// If there are parsing errors, log them and throw an error
const errorMessages = Array.from(parseErrors)
.map((errorElement, index) => {
return `Parsing Error ${index + 1}: ${errorElement.textContent}`;
})
.join("\n");
console.error(errorMessages);
// Throw an error to indicate parsing failure
throw new Error("Failed to parse KML. See console for details.");
}
// If parsing is successful, return the parsed XML document
return xmlDoc;
};
/**
* Converts a KML document to a GeoJSON FeatureCollection.
* @param {Document} document - The KML document to convert.
* @param {boolean} includeCrs - Optional boolean to determine if CRS should be included.
* @returns {Object} - The resulting GeoJSON FeatureCollection.
*
* NOTE:
* KML files inherently assume the use of the EPSG:4326 (WGS 84) coordinate reference system
* for all geographic coordinates. As such, when converting from KML to GeoJSON, the coordinates
* are retained in the standard WGS 84 format.
*
* The GeoJSON output will conform to this CRS standard, and no additional CRS transformation are needed.
* Users can rely on the spatial information being accurate with respect to the WGS 84 datum.
*
* Additionally, this function includes an option to add CRS information explicitly to the GeoJSON output (none standard).
* By setting the `includeCrs` parameter to `true`, the resulting GeoJSON will include a 'crs' property
* that specifies the use of EPSG:4326: the geoJSON standard.
*
* crs: {
* type: "name",
* properties: {
* name: "EPSG:4326",
* },
* }
*/
GeoKMLer.prototype.toGeoJSON = function (document, includeCrs = false) {
const features = [];
for (const placemark of document.getElementsByTagName("Placemark")) {
features.push(...this.handlePlacemark(placemark));
}
const geoJson = {
type: "FeatureCollection",
features: features,
};
if (includeCrs) {
geoJson.crs = {
type: "name",
properties: {
name: "EPSG:4326",
},
};
}
return geoJson;
};
/**
* Processes a KML Placemark and converts its geometries to GeoJSON features.
* @param {Element} placemark - The Placemark element to process.
* @returns {Array} - An array of GeoJSON features.
*/
GeoKMLer.prototype.handlePlacemark = function (placemark) {
const features = [];
const properties = this.extractProperties(placemark);
// Merge extended data directly into the properties without an additional 'ExtendedData' entry
Object.assign(properties, this.extractExtendedData(placemark));
for (let i = 0; i < placemark.children.length; i++) {
const element = placemark.children[i];
switch (element.tagName) {
case "Point":
features.push(this.pointToPoint(element, placemark, properties));
break;
case "LineString":
features.push(this.lineStringToLineString(element, placemark, properties));
break;
case "Polygon":
features.push(this.polygonToPolygon(element, placemark, properties));
break;
case "MultiGeometry":
features.push(...this.handleMultiGeometry(element, placemark, properties));
break;
}
}
return features;
};
/**
* Converts coordinate strings into arrays of [longitude, latitude].
* @param {string} coordString - The coordinate string from KML.
* @returns {Array} - An array of [longitude, latitude] pairs.
*/
GeoKMLer.prototype.coordFromString = function(coordString) {
return coordString.trim().split(/\s+/).map(coord => {
const [lon, lat, ele] = coord.split(',').map(parseFloat);
return [lon, lat, ele]; // Include ele for elevation
});
};
/**
* Parses a single coordinate string into a numeric array.
* @param {string} v - The coordinate string.
* @returns {Array} - An array of parsed coordinate values.
*/
GeoKMLer.prototype.coord1 = function (v) {
const removeSpace = /\s*/g;
return v.replace(removeSpace, "").split(",").map(parseFloat);
};
/**
* Parses multiple coordinate strings into an array of coordinate arrays.
* @param {string} v - The coordinate string with multiple coordinates.
* @returns {Array} - A nested array of parsed coordinate values.
*/
GeoKMLer.prototype.coord = function (v) {
const trimSpace = /^\s*|\s*$/g;
const splitSpace = /\s+/;
const coords = v.replace(trimSpace, "").split(splitSpace);
return coords.map((coord) => this.coord1(coord));
};
/**
* Extracts extended data from a KML placemark.
* @param {Element} placemark - The Placemark element to extract from.
* @returns {Object} - An object containing extended data properties.
*/
GeoKMLer.prototype.extractExtendedData = function (placemark) {
const extendedData = {};
const extendedDataTag = this.getChildNode(placemark, "ExtendedData");
if (!extendedDataTag) return extendedData;
const simpleDatas = this.getChildNodes(extendedDataTag, "SimpleData");
simpleDatas.forEach((data) => {
const name = data.getAttribute("name");
const value = this.nodeVal(data);
if (name && value !== null) {
extendedData[`ex_${name}`] = value.trim();
}
});
return extendedData;
};
/**
* Fetches the value of a text node.
* @param {Node} x - The node to extract the value from.
* @returns {string} - The text content of the node.
*/
GeoKMLer.prototype.nodeVal = function (x) {
return x ? x.textContent || "" : "";
};
/**
* Retrieves a single child node of a specified tag name.
* @param {Element} x - The parent element.
* @param {string} y - The tag name of the child node.
* @returns {Element|null} - The first matching child node or null if none are found.
*/
GeoKMLer.prototype.getChildNode = function (x, y) {
const nodeList = x.getElementsByTagName(y);
return nodeList.length ? nodeList[0] : null;
};
/**
* Retrieves all child nodes of a specified tag name.
* @param {Element} x - The parent element.
* @param {string} y - The tag name of the child nodes.
* @returns {Array} - An array of matching child nodes.
*/
GeoKMLer.prototype.getChildNodes = function (x, y) {
return Array.from(x.getElementsByTagName(y));
};
/**
* Retrieves an attribute value from an element.
* @param {Element} x - The element to extract the attribute from.
* @param {string} y - The name of the attribute.
* @returns {string|null} - The attribute value or null if not present.
*/
GeoKMLer.prototype.attr = function (x, y) {
return x.getAttribute(y);
};
/**
* Retrieves a floating-point attribute value from an element.
* @param {Element} x - The element to extract the attribute from.
* @param {string} y - The name of the attribute.
* @returns {number} - The parsed floating-point attribute value.
*/
GeoKMLer.prototype.attrf = function (x, y) {
return parseFloat(this.attr(x, y));
};
/**
* Normalizes an XML node to combine adjacent text nodes.
* @param {Node} el - The XML node to normalize.
* @returns {Node} - The normalized node.
*/
GeoKMLer.prototype.norm = function (el) {
if (el.normalize) el.normalize();
return el;
};
/**
* Creates a GeoJSON feature for a given geometry type and coordinates.
* @param {string} type - The geometry type (Point, LineString, Polygon).
* @param {Array} coords - The coordinates for the geometry.
* @param {Object} props - The properties of the feature.
* @returns {Object} - The created GeoJSON feature.
*/
GeoKMLer.prototype.makeFeature = function (type, coords, props) {
return {
type: "Feature",
geometry: {
type: type,
coordinates: coords,
},
properties: props,
};
};
/**
* Converts a KML Point to a GeoJSON Point feature.
* @param {Element} node - The Point element.
* @param {Element} placemark - The parent Placemark element.
* @param {Object} props - The properties of the feature.
* @returns {Object} - A GeoJSON Point feature.
*/
GeoKMLer.prototype.pointToPoint = function (node, placemark, props) {
const coord = this.coordFromString(node.getElementsByTagName("coordinates")[0].textContent)[0];
return this.makeFeature("Point", coord, props);
};
/**
* Converts a KML LineString to a GeoJSON LineString feature.
* @param {Element} node - The LineString element.
* @param {Element} placemark - The parent Placemark element.
* @param {Object} props - The properties of the feature.
* @returns {Object} - A GeoJSON LineString feature.
*/
GeoKMLer.prototype.lineStringToLineString = function (node, placemark, props) {
const coords = this.coordFromString(node.getElementsByTagName("coordinates")[0].textContent);
return this.makeFeature("LineString", coords, props);
};
/**
* Converts a KML Polygon to a GeoJSON Polygon feature.
* @param {Element} node - The Polygon element.
* @param {Element} placemark - The parent Placemark element.
* @param {Object} props - The properties of the feature.
* @returns {Object} - A GeoJSON Polygon feature.
*/
GeoKMLer.prototype.polygonToPolygon = function (node, placemark, props) {
const coords = [];
for (const boundary of node.getElementsByTagName("LinearRing")) {
coords.push(this.coordFromString(boundary.getElementsByTagName("coordinates")[0].textContent));
}
return this.makeFeature("Polygon", coords, props);
};
/**
* Processes a MultiGeometry and converts its geometries to GeoJSON features.
* @param {Element} node - The MultiGeometry element.
* @param {Element} placemark - The parent Placemark element.
* @param {Object} props - The properties of the features.
* @returns {Array} - An array of GeoJSON features.
*/
GeoKMLer.prototype.handleMultiGeometry = function (node, placemark, props) {
const features = [];
for (const element of node.children) {
switch (element.tagName) {
case "Point":
features.push(this.pointToPoint(element, placemark, props));
break;
case "LineString":
features.push(this.lineStringToLineString(element, placemark, props));
break;
case "Polygon":
features.push(this.polygonToPolygon(element, placemark, props));
break;
case "MultiGeometry":
features.push(...this.handleMultiGeometry(element, placemark, props));
break;
}
}
return features;
};
/**
* Extracts properties from a Placemark, excluding geometry elements.
* @param {Element} placemark - The Placemark element to extract properties from.
* @returns {Object} - An object containing placemark properties.
*/
GeoKMLer.prototype.extractProperties = function (placemark) {
const props = {};
for (const n of placemark.children) {
if (!["Point", "LineString", "Polygon", "MultiGeometry", "LinearRing", "style", "styleMap", "styleUrl", "TimeSpan", "TimeStamp"].includes(n.tagName)) {
// Ensure "ExtendedData" is not added directly.
if (n.tagName !== "ExtendedData") {
props[n.tagName] = n.textContent.trim();
}
}
}
return props;
};
return GeoKMLer;
})();