이 스크립트는 직접 설치해서 쓰는 게 아닙니다. 다른 스크립트가 메타 명령 // @require https://update.greasyfork.org/scripts/526229/1537672/GeoGMLer.js
(으)로 포함하여 쓰는 라이브러리입니다.
// ==UserScript==
// @name GeoGMLer
// @namespace https://github.com/JS55CT
// @description GeoGMLer is a JavaScript library for converting GML data into GeoJSON. It translates FeatureMembers with Points, LineStrings, and Polygons, handling coordinates via gml:coordinates and gml:posList. Supports multi-geometries to ensure conversion to GeoJSON's FeatureCollection.
// @version 2.1.0
// @author JS55CT
// @license MIT
// @match *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==
/***********************************************************
* ## Project Home < https://github.com/JS55CT/GeoGMLer >
* 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.
*
* - Project was inspired by the work of [gml2geojson](https://github.com/deyihu/gml2geojson) (MIT licensed)
* and builds upon the concepts and implementations found there
**************************************************************/
/** TO DO: intergrate pro4.js to convert to ESPG:4326 standerd
* The default Coordinate Reference System (CRS) for GeoJSON is WGS 84, which is represented by the EPSG code 4326.
* This means that coordinates in a GeoJSON file are expected to be in longitude and latitude format, following the WGS 84 datum.
* In the GeoJSON format, the coordinates are typically ordered as [longitude, latitude].
* It's important to adhere to this order to ensure proper geospatial data interpretation and interoperability with GIS tools
* and applications that conform to the GeoJSON specification.
*
* While GeoJSON does allow specifying other coordinate reference systems through extensions,
* the use of any CRS other than WGS 84 is not recommended as it breaks the convention and could impact interoperability
* and usability across services and applications that expect WGS 84.
*/
var GeoGMLer = (function () {
/**
* GeoGMLer constructor function.
* @returns {GeoGMLer} - An instance of GeoGMLer.
*/
function GeoGMLer(obj) {
if (obj instanceof GeoGMLer) return obj;
if (!(this instanceof GeoGMLer)) return new GeoGMLer(obj);
this._wrapped = obj;
}
const GEONODENAMES = ["geometryproperty", "geometryProperty"];
/**
* Reads a GML string and prepares it for conversion by extracting
* both the parsed XML document and its coordinate reference system (CRS).
* @param {string} str - The GML string to read.
* @returns {Object} - An object containing the parsed XML document and CRS name.
* @property {Document} xmlDoc - The parsed XML document.
* @property {string} crsName - The name of the coordinate reference system extracted from the GML.
*/
GeoGMLer.prototype.read = function (gmlText) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(gmlText, "application/xml");
// Check for parsing errors by looking for parser error tags
const parseErrors = xmlDoc.getElementsByTagName("parsererror");
if (parseErrors.length > 0) {
const errorMessages = Array.from(parseErrors)
.map((errorElement, index) => {
return `Parsing Error ${index + 1}: ${errorElement.textContent}`;
})
.join("\n");
console.error(errorMessages);
throw new Error("Failed to parse GML. See console for details.");
}
// Extract the CRS directly within the read function if parsing is successful
const crsName = this.getCRS(xmlDoc);
// Return both the XML document and the CRS
return {
xmlDoc,
crsName,
};
};
/**
* Converts a parsed GML XML document to a GeoJSON object, incorporating the specified CRS.
* @param {Object} params - The parameters required for conversion.
* @param {Document} params.xmlDoc - The parsed XML document to convert.
* @param {string} params.crsName - The name of the coordinate reference system.
* @returns {Object} - The GeoJSON object representing the features and their CRS.
*
* WARNING: The input GML geometries may specify a spatial reference system (SRS) through the `srsName` attribute.
* This function extracts the `srsName` and includes it in the GeoJSON output under the 'crs' property:
*
* crs: {
* type: "name",
* properties: {
* name: crsName,
* },
* },
*
* However, the function does not transform the coordinate values to match the EPSG:4326 (WGS 84) geoJSON standard.
* This means the coordinate values remain in the original SRS specified by `srsName`.
* Users should be aware that the GeoJSON output may not conform to expected standards if the original SRS
* is not compatible with their intended use. It is essential to handle coordinate transformations as needed
* for accurate spatial data representation.
*/
GeoGMLer.prototype.toGeoJSON = function ({ xmlDoc, crsName }) {
const geojson = {
type: "FeatureCollection",
features: [],
crs: {
type: "name",
properties: {
name: crsName,
},
},
};
// Get the main element of the feature collection
const featureCollectionEle = xmlDoc.children[0];
// Check if the node is a FeatureCollection, considering possible namespace prefixes
const nodeName = this.getNodeName(featureCollectionEle); // this returns lowercase by default
const isFeatureCollection = featureCollectionEle && featureCollectionEle.nodeName && nodeName.includes("featurecollection");
// Validate the document structure
if (!isFeatureCollection) {
console.error("Invalid GML structure: The document does not contain a valid FeatureCollection element.");
return geojson; // Return empty GeoJSON if the structure is incorrect
}
const features = [];
// Iterate over each child node to extract feature members
for (let i = 0; i < featureCollectionEle.children.length; i++) {
const featureEle = featureCollectionEle.children.item(i);
if (featureEle) {
const childNodeName = this.getNodeName(featureEle);
// Identify and collect feature member elements
if (childNodeName.includes("featuremember") && featureEle.children[0]) {
features.push(featureEle.children[0]);
}
}
}
// Process each feature member to extract properties and geometry
for (let i = 0, len = features.length; i < len; i++) {
const f = features[i];
const properties = this.getFeatureEleProperties(f); // Extract properties
const geometry = this.getFeatureEleGeometry(f, crsName); // Extract geometry using the provided CRS
if (!geometry || !properties) {
console.error(`Skipping feature ${i + 1} due to missing geometry or properties.`);
continue; // Skip if geometry or properties are missing
}
const feature = {
type: "Feature",
geometry,
properties,
};
geojson.features.push(feature); // Add the feature to the GeoJSON features array
}
return geojson; // Return the constructed GeoJSON object
};
/**
* Retrieves the CRS (Coordinate Reference System) from GML.
* @param {string} gmlString - The GML string.
* @returns {string|null} - The CRS name or null.
*/
// Enhanced getCRS function to search for srsName attribute in various geometry nodes
GeoGMLer.prototype.getCRS = function (xmlDoc) {
// Define a list of common GML geometry elements to check for srsName attribute
const geometryTags = [
"gml:Envelope",
"gml:Point",
"gml:LineString",
"gml:Polygon",
"gml:MultiPoint",
"gml:MultiLineString",
"gml:MultiPolygon",
"gml:Surface",
"gml:Solid",
// Add other geometry types as needed
];
for (const tag of geometryTags) {
const elements = xmlDoc.getElementsByTagName(tag);
for (let i = 0; i < elements.length; i++) {
const srsName = elements[i].getAttribute("srsName");
if (srsName) {
return srsName.trim();
}
}
}
// Consider additional handling or logging if no srsName is found
return null;
};
/**
* Extracts the geometry from a GML feature element.
* @param {Element} featureEle - The feature element.
* @param {string} crsName - The name of the CRS.
* @returns {Object|null} - The geometry object or null.
*/
GeoGMLer.prototype.getFeatureEleGeometry = function (featureEle, crsName) {
const children = featureEle.children || [];
let type;
let coordinates = [];
for (let i = 0, len = children.length; i < len; i++) {
const node = children[i];
const nodeName = this.getNodeName(node);
if (!this.isGeoAttribute(nodeName)) {
continue;
}
if (node.children && node.children[0]) {
type = node.children[0].nodeName.split("gml:")[1] || "";
if (!type) {
continue;
}
const geoElement = node.children[0];
if (type === "Point") {
coordinates = this.processPoint(geoElement, crsName);
break;
} else if (type === "MultiPoint") {
coordinates = this.processMultiPoint(geoElement, crsName);
break;
} else if (type === "MultiSurface" || type === "MultiPolygon") {
coordinates = this.processMultiSurface(geoElement, crsName);
break;
} else if (type === "MultiCurve" || type === "MultiLineString") {
coordinates = this.processMultiCurve(geoElement, crsName);
break;
} else if (geoElement.children.length > 0) {
let geoNodes = Array.from(geoElement.children);
if (this.isMulti(this.getNodeName(geoNodes[0]))) {
geoNodes = this.flatMultiGeoNodes(geoNodes);
}
if (geoNodes.length) {
geoNodes.forEach((geoNode) => {
let coords = this.parseGeoCoordinates(geoNode.children, crsName);
if (!this.geoIsPolygon(type) && this.isMultiLine(type)) {
coords = coords[0];
}
coordinates.push(coords);
});
break;
}
}
}
}
if (!type || !coordinates.length) {
return null;
}
return {
type: this.mapGmlTypeToGeoJson(type),
coordinates,
};
};
/**
* Processes a multi-surface element to extract polygons.
* @param {Element} multiSurfaceElement - The multi-surface element.
* @param {string} crsName - The name of the CRS.
* @returns {Array} - Array of polygons.
*/
GeoGMLer.prototype.processMultiSurface = function (multiSurfaceElement, crsName) {
const polygons = [];
const surfaceMembers = multiSurfaceElement.getElementsByTagName("gml:surfaceMember");
for (let j = 0; j < surfaceMembers.length; j++) {
const polygon = this.processPolygon(surfaceMembers[j].getElementsByTagName("gml:Polygon")[0], crsName);
if (polygon) {
polygons.push(polygon);
}
}
return polygons;
};
/**
* Processes a polygon element.
* @param {Element} polygonElement - The polygon element.
* @param {string} crsName - The name of the CRS.
* @returns {Array} - Array representing the polygon.
*/
GeoGMLer.prototype.processPolygon = function (polygonElement, crsName) {
const polygon = [];
const exteriorElements = polygonElement.getElementsByTagName("gml:exterior");
if (exteriorElements.length > 0) {
const exterior = this.parseRing(exteriorElements[0], crsName);
if (exterior) {
polygon.push(exterior);
}
}
const interiorElements = polygonElement.getElementsByTagName("gml:interior");
for (let k = 0; k < interiorElements.length; k++) {
const interior = this.parseRing(interiorElements[k], crsName);
if (interior) {
polygon.push(interior);
}
}
return polygon;
};
/**
* Parses a ring element to extract coordinates.
* @param {Element} ringElement - The ring element.
* @param {string} crsName - The name of the CRS.
* @returns {Array} - Array of coordinates.
*/
GeoGMLer.prototype.parseRing = function (ringElement, crsName) {
const coordNodes = ringElement.getElementsByTagName("gml:posList");
if (coordNodes.length > 0) {
return this.parseGeoCoordinates(coordNodes, crsName);
}
return [];
};
/**
* Processes a multi-curve element to extract line strings.
* @param {Element} multiCurveElement - The multi-curve element.
* @param {string} crsName - The name of the CRS.
* @returns {Array} - Array of line strings.
*/
GeoGMLer.prototype.processMultiCurve = function (multiCurveElement, crsName) {
const lineStrings = [];
const curveMembers = multiCurveElement.getElementsByTagName("gml:curveMember");
for (let j = 0; j < curveMembers.length; j++) {
const lineStringElement = curveMembers[j].getElementsByTagName("gml:LineString")[0];
if (lineStringElement) {
const lineString = this.processLineString(lineStringElement, crsName);
if (lineString) {
lineStrings.push(lineString);
}
}
}
return lineStrings;
};
/**
* Processes a line string element.
* @param {Element} lineStringElement - The line string element.
* @param {string} crsName - The name of the CRS.
* @returns {Array} - Array of coordinates representing the line string.
*/
GeoGMLer.prototype.processLineString = function (lineStringElement, crsName) {
const coordNodes = lineStringElement.getElementsByTagName("gml:posList");
if (coordNodes.length > 0) {
return this.parseGeoCoordinates(coordNodes, crsName);
}
return [];
};
/**
* Processes a GML Point geometry element to extract its coordinates.
*
* @param {Element} geoElement - The GML element representing the Point geometry.
* @param {string} crsName - The coordinate reference system (CRS) name, used to determine if coordinate order needs to be reversed.
*
* @returns {Array} An array containing the coordinates for the Point. If no valid coordinates are found, an empty array is returned.
*
* The function first attempts to find the coordinates using the `<gml:pos>` element. If that is not available,
* it looks for the `<gml:coordinates>` element instead. It utilizes the `parseGeoCoordinates` method to convert
* the raw coordinate text into an array of numbers, considering the specified CRS.
*/
GeoGMLer.prototype.processPoint = function (geoElement, crsName) {
let coordNode = geoElement.getElementsByTagName("gml:pos");
if (coordNode.length === 0) {
coordNode = geoElement.getElementsByTagName("gml:coordinates");
}
if (coordNode.length > 0) {
// Parse the coordinates
const parsedCoords = this.parseGeoCoordinates([coordNode[0]], crsName);
// Flatten them if necessary (should only be length 1 for a valid Point)
return parsedCoords.length > 0 ? parsedCoords[0] : [];
} else {
return [];
}
};
/**
* Processes a multi-point element to extract the coordinates of each point.
* @param {Element} multiPointElement - The element representing the MultiPoint geometry.
* @param {string} crsName - The coordinate reference system (CRS) name.
* @returns {Array} - An array of coordinate arrays for the multipoint.
*/
GeoGMLer.prototype.processMultiPoint = function (multiPointElement, crsName) {
const points = [];
const pointMembers = multiPointElement.getElementsByTagName("gml:pointMember");
for (let j = 0; j < pointMembers.length; j++) {
const pointElement = pointMembers[j].getElementsByTagName("gml:Point")[0];
if (pointElement) {
const coordinates = this.processPoint(pointElement, crsName);
if (coordinates.length > 0) {
points.push(coordinates);
}
}
}
return points;
};
/**
* Parses coordinate nodes into arrays of coordinates, considering the coordinate reference system (CRS)
* and the coordinate formatting requirements.
*
* @param {HTMLCollection} coordNodes - The collection of coordinate nodes to be parsed.
* @param {string} crsName - The name of the coordinate reference system (CRS).
* @returns {Array} - An array of parsed coordinates.
*
* The `needsReversal` flag is determined by the CRS name. It is set to true for common geographic
* coordinate systems like "EPSG:4326", "CRS84", or "WGS84", which typically use a "latitude, longitude"
* format. Reversing is necessary when converting to systems expecting the "longitude, latitude" order
* like geoJSON.
*
* The `isCommaSeparated` flag is used to determine the delimiter in the coordinate parsing. It checks
* if the coordinates node is named with ":coordinates", which indicates that commas are used to
* separate coordinate values (older versions of GML). This is essential for correctly interpreting data where commas are
* the delimiter, distinguishing from systems using whitespace (GML3.X) with :pos and :posList.
*/
GeoGMLer.prototype.parseGeoCoordinates = function (coordNodes, crsName) {
const coordinates = [];
const needsReversal = crsName.includes("4326") || crsName.includes("CRS84") || crsName.includes("WGS84");
if (coordNodes.length === 0) {
}
for (let i = 0, len = coordNodes.length; i < len; i++) {
const coordNode = this.findCoordsNode(coordNodes[i]);
if (!coordNode) {
continue;
}
const isCommaSeparated = this.getNodeName(coordNode).indexOf(":coordinates") > -1;
const textContent = coordNode.textContent.trim();
const coords = this.parseCoordinates(textContent, isCommaSeparated, needsReversal);
coordinates.push(...coords);
}
return coordinates;
};
/**
* Parses a coordinate string into an array of coordinate pairs, considering whether the input
* uses commas as separators and whether the coordinate pair order needs to be reversed.
*
* @param {string} text - The text containing coordinates, which may be in different formats
* based on the input data (e.g., comma-separated or space-separated).
* @param {boolean} isCommaSeparated - A flag indicating if the coordinate string uses commas as
* separators between individual coordinates. This is often observed in older data formats where
* coordinates are presented as "x,y".
* @param {boolean} needsReversal - A flag indicating if the latitude and longitude values need to
* be reversed in order, which is particularly necessary for compatibility with modern formats like
* GeoJSON. Older versions of GML (such as 1 and 2), when using the :coordinates tag and a CRS like
* "EPSG:4326", often present coordinates in "latitude, longitude" format. In contrast, GeoJSON and
* other modern systems require "longitude, latitude". However, in GML 3.x, it is more common to use
* elements like :pos and :posList, which typically follow the "longitude, latitude" order, aligning
* with modern geographic data representations regardless of the projection system used.
* @returns {Array} - An array of coordinate pairs, where each pair is represented as an array of the
* form [longitude, latitude] or [latitude, longitude] depending on the `needsReversal` flag.
*/
GeoGMLer.prototype.parseCoordinates = function (text, isCommaSeparated, needsReversal) {
if (!text) return [];
const coords = text.trim().split(/\s+/);
const coordinates = [];
for (let i = 0; i < coords.length; i++) {
let c1, c2;
const coord = coords[i];
if (isCommaSeparated) {
if (coord.includes(",")) {
const [x, y] = coord.split(",");
c1 = this.trimAndParse(x);
c2 = this.trimAndParse(y);
coordinates.push(needsReversal ? [c1, c2] : [c2, c1]);
}
} else {
c1 = this.trimAndParse(coord);
c2 = this.trimAndParse(coords[i + 1]);
i++; // Skip the next coordinate since it's already processed
coordinates.push(needsReversal ? [c2, c1] : [c1, c2]);
}
}
return coordinates;
};
/**
* Trims and parses a string into a float.
* @param {string} str - The string to parse.
* @returns {number} - The parsed float.
*/
GeoGMLer.prototype.trimAndParse = function (str) {
return parseFloat(str.replace(/\s+/g, ""));
};
/**
* Finds the coordinate node within a given node.
* @param {Node} node - The node to search.
* @returns {Node} - The coordinate node found.
*/
GeoGMLer.prototype.findCoordsNode = function (node) {
let nodeName = this.getNodeName(node);
while (nodeName.indexOf(":coordinates") === -1 && nodeName.indexOf(":posList") === -1 && nodeName.indexOf(":pos") === -1) {
node = node.children[0];
nodeName = this.getNodeName(node);
}
return node;
};
/**
* Retrieves the node name.
* @param {Node} node - The node object.
* @param {boolean} [lowerCase=true] - Whether to convert the name to lower case.
* @returns {string} - The node name.
*/
GeoGMLer.prototype.getNodeName = function (node, lowerCase = true) {
if (lowerCase) {
return (node.nodeName || "").toLocaleLowerCase();
} else {
return node.nodeName || "";
}
};
/**
* Checks if the geometry type is a polygon.
* @param {string} type - The geometry type.
* @returns {boolean} - True if the type is a polygon.
*/
GeoGMLer.prototype.geoIsPolygon = function (type) {
return type.indexOf("Polygon") > -1;
};
/**
* Maps GML geometry types to GeoJSON types.
* @param {string} type - The GML type.
* @returns {string} - The corresponding GeoJSON type.
*/
GeoGMLer.prototype.mapGmlTypeToGeoJson = function (type) {
switch (type) {
case "MultiCurve":
return "MultiLineString";
case "MultiSurface":
return "MultiPolygon";
default:
return type; // Return as-is for matching types
}
};
/**
* Extracts feature element properties.
* @param {Element} featureEle - The feature element.
* @returns {Object} - The properties object.
*/
GeoGMLer.prototype.getFeatureEleProperties = function (featureEle) {
const children = featureEle.children || [];
const properties = {};
for (let i = 0, len = children.length; i < len; i++) {
const node = children[i];
const nodeName = this.getNodeName(node);
// Skip geometry-related attributes
if (this.isGeoAttribute(nodeName) && node.children.length) {
continue;
}
// Skip boundedBy or other GML-specific elements
if (nodeName === "gml:boundedby" || nodeName === "gml:geometryproperty") {
continue;
}
// Extract feature properties
const key = node.nodeName.includes(":") ? node.nodeName.split(":")[1] : node.nodeName;
if (!key) {
continue;
}
const value = node.textContent || "";
properties[key] = value;
}
return properties;
};
/**
* Flattens multi-geometry nodes.
* @param {Array} nodes - The multi-geometry nodes.
* @returns {Array} - Array of geometry nodes.
*/
GeoGMLer.prototype.flatMultiGeoNodes = function (nodes) {
const geoNodes = [];
for (let i = 0, len = nodes.length; i < len; i++) {
const children = nodes[i].children;
for (let j = 0, len1 = children.length; j < len1; j++) {
geoNodes.push(children[j].children[0]);
}
}
return geoNodes;
};
/**
* Checks if the node name indicates a multi-geometry.
* @param {string} nodeName - The node name.
* @returns {boolean} - True if the node denotes a multi-geometry.
*/
GeoGMLer.prototype.isMulti = function (nodeName) {
return nodeName.indexOf("member") > -1;
};
/**
* Checks if the geometry type is a multi-line.
* @param {string} type - The geometry type.
* @returns {boolean} - True if the type is a multi-line.
*/
GeoGMLer.prototype.isMultiLine = function (type) {
return type === "MultiCurve" || type === "MultiLineString";
};
/**
* Checks if the node name is a geometry attribute.
* @param {string} nodeName - The node name.
* @returns {boolean} - True if the attribute is geometry-related.
*/
GeoGMLer.prototype.isGeoAttribute = function (nodeName) {
return GEONODENAMES.some((geoName) => nodeName.indexOf(geoName) > -1);
};
return GeoGMLer;
})();