UNPKG

@tmcw/togeojson

Version:
1,132 lines (1,116 loc) 35.1 kB
'use strict'; function $(element, tagName) { return Array.from(element.getElementsByTagName(tagName)); } function normalizeId(id) { return id[0] === "#" ? id : `#${id}`; } function $ns(element, tagName, ns) { return Array.from(element.getElementsByTagNameNS(ns, tagName)); } /** * get the content of a text node, if any */ function nodeVal(node) { node?.normalize(); return node?.textContent || ""; } /** * Get one Y child of X, if any, otherwise null */ function get1(node, tagName, callback) { const n = node.getElementsByTagName(tagName); const result = n.length ? n[0] : null; if (result && callback) callback(result); return result; } function get(node, tagName, callback) { const properties = {}; if (!node) return properties; const n = node.getElementsByTagName(tagName); const result = n.length ? n[0] : null; if (result && callback) { return callback(result, properties); } return properties; } function val1(node, tagName, callback) { const val = nodeVal(get1(node, tagName)); if (val && callback) return callback(val) || {}; return {}; } function $num(node, tagName, callback) { const val = Number.parseFloat(nodeVal(get1(node, tagName))); if (Number.isNaN(val)) return undefined; if (val && callback) return callback(val) || {}; return {}; } function num1(node, tagName, callback) { const val = Number.parseFloat(nodeVal(get1(node, tagName))); if (Number.isNaN(val)) return undefined; if (callback) callback(val); return val; } function getMulti(node, propertyNames) { const properties = {}; for (const property of propertyNames) { val1(node, property, (val) => { properties[property] = val; }); } return properties; } function isElement(node) { return node?.nodeType === 1; } function getExtensions(node) { let values = []; if (node === null) return values; for (const child of Array.from(node.childNodes)) { if (!isElement(child)) continue; const name = abbreviateName(child.nodeName); if (name === "gpxtpx:TrackPointExtension") { // loop again for nested garmin extensions (eg. "gpxtpx:hr") values = values.concat(getExtensions(child)); } else { // push custom extension (eg. "power") const val = nodeVal(child); values.push([name, parseNumeric(val)]); } } return values; } function abbreviateName(name) { return ["heart", "gpxtpx:hr", "hr"].includes(name) ? "heart" : name; } function parseNumeric(val) { const num = Number.parseFloat(val); return Number.isNaN(num) ? val : num; } function coordPair$1(node) { const ll = [ Number.parseFloat(node.getAttribute("lon") || ""), Number.parseFloat(node.getAttribute("lat") || ""), ]; if (Number.isNaN(ll[0]) || Number.isNaN(ll[1])) { return null; } num1(node, "ele", (val) => { ll.push(val); }); const time = get1(node, "time"); return { coordinates: ll, time: time ? nodeVal(time) : null, extendedValues: getExtensions(get1(node, "extensions")), }; } function getLineStyle(node) { return get(node, "line", (lineStyle) => { const val = Object.assign({}, val1(lineStyle, "color", (color) => { return { stroke: `#${color}` }; }), $num(lineStyle, "opacity", (opacity) => { return { "stroke-opacity": opacity }; }), $num(lineStyle, "width", (width) => { // GPX width is in mm, convert to px with 96 px per inch return { "stroke-width": (width * 96) / 25.4 }; })); return val; }); } function extractProperties(ns, node) { const properties = getMulti(node, [ "name", "cmt", "desc", "type", "time", "keywords", ]); for (const [n, url] of ns) { for (const child of Array.from(node.getElementsByTagNameNS(url, "*"))) { properties[child.tagName.replace(":", "_")] = nodeVal(child)?.trim(); } } const links = $(node, "link"); if (links.length) { properties.links = links.map((link) => Object.assign({ href: link.getAttribute("href") }, getMulti(link, ["text", "type"]))); } return properties; } /** * Extract points from a trkseg or rte element. */ function getPoints$1(node, pointname) { const pts = $(node, pointname); const line = []; const times = []; const extendedValues = {}; for (let i = 0; i < pts.length; i++) { const c = coordPair$1(pts[i]); if (!c) { continue; } line.push(c.coordinates); if (c.time) times.push(c.time); for (const [name, val] of c.extendedValues) { const plural = name === "heart" ? name : `${name.replace("gpxtpx:", "")}s`; if (!extendedValues[plural]) { extendedValues[plural] = Array(pts.length).fill(null); } extendedValues[plural][i] = val; } } if (line.length < 2) return; // Invalid line in GeoJSON return { line: line, times: times, extendedValues: extendedValues, }; } /** * Extract a LineString geometry from a rte * element. */ function getRoute(ns, node) { const line = getPoints$1(node, "rtept"); if (!line) return; return { type: "Feature", properties: Object.assign({ _gpxType: "rte" }, extractProperties(ns, node), getLineStyle(get1(node, "extensions"))), geometry: { type: "LineString", coordinates: line.line, }, }; } function getTrack(ns, node) { const segments = $(node, "trkseg"); const track = []; const times = []; const extractedLines = []; for (const segment of segments) { const line = getPoints$1(segment, "trkpt"); if (line) { extractedLines.push(line); if (line.times?.length) times.push(line.times); } } if (extractedLines.length === 0) return null; const multi = extractedLines.length > 1; const properties = Object.assign({ _gpxType: "trk" }, extractProperties(ns, node), getLineStyle(get1(node, "extensions")), times.length ? { coordinateProperties: { times: multi ? times : times[0], }, } : {}); for (const line of extractedLines) { track.push(line.line); if (!properties.coordinateProperties) { properties.coordinateProperties = {}; } const props = properties.coordinateProperties; const entries = Object.entries(line.extendedValues); for (let i = 0; i < entries.length; i++) { const [name, val] = entries[i]; if (multi) { if (!props[name]) { props[name] = extractedLines.map((line) => new Array(line.line.length).fill(null)); } props[name][i] = val; } else { props[name] = val; } } } return { type: "Feature", properties: properties, geometry: multi ? { type: "MultiLineString", coordinates: track, } : { type: "LineString", coordinates: track[0], }, }; } /** * Extract a point, if possible, from a given node, * which is usually a wpt or trkpt */ function getPoint(ns, node) { const properties = Object.assign(extractProperties(ns, node), getMulti(node, ["sym"])); const pair = coordPair$1(node); if (!pair) return null; return { type: "Feature", properties, geometry: { type: "Point", coordinates: pair.coordinates, }, }; } /** * Convert GPX to GeoJSON incrementally, returning * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) * that yields output feature by feature. */ function* gpxGen(node) { const n = node; const GPXX = "gpxx"; const GPXX_URI = "http://www.garmin.com/xmlschemas/GpxExtensions/v3"; // Namespaces const ns = [[GPXX, GPXX_URI]]; const attrs = n.getElementsByTagName("gpx")[0]?.attributes; if (attrs) { for (const attr of Array.from(attrs)) { if (attr.name?.startsWith("xmlns:") && attr.value !== GPXX_URI) { ns.push([attr.name, attr.value]); } } } for (const track of $(n, "trk")) { const feature = getTrack(ns, track); if (feature) yield feature; } for (const route of $(n, "rte")) { const feature = getRoute(ns, route); if (feature) yield feature; } for (const waypoint of $(n, "wpt")) { const point = getPoint(ns, waypoint); if (point) yield point; } } /** * * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX * document as an XML DOM - not as a string. You can get this using jQuery's default * `.ajax` function or using a bare XMLHttpRequest with the `.response` property * holding an XML DOM. * * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the * addition of a `_gpxType` property on each `LineString` feature that indicates whether * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document. */ function gpx(node) { return { type: "FeatureCollection", features: Array.from(gpxGen(node)), }; } const EXTENSIONS_NS = "http://www.garmin.com/xmlschemas/ActivityExtension/v2"; const TRACKPOINT_ATTRIBUTES = [ ["heartRate", "heartRates"], ["Cadence", "cadences"], // Extended Trackpoint attributes ["Speed", "speeds"], ["Watts", "watts"], ]; const LAP_ATTRIBUTES = [ ["TotalTimeSeconds", "totalTimeSeconds"], ["DistanceMeters", "distanceMeters"], ["MaximumSpeed", "maxSpeed"], ["AverageHeartRateBpm", "avgHeartRate"], ["MaximumHeartRateBpm", "maxHeartRate"], // Extended Lap attributes ["AvgSpeed", "avgSpeed"], ["AvgWatts", "avgWatts"], ["MaxWatts", "maxWatts"], ]; function getProperties(node, attributeNames) { const properties = []; for (const [tag, alias] of attributeNames) { let elem = get1(node, tag); if (!elem) { const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag); if (elements.length) { elem = elements[0]; } } const val = Number.parseFloat(nodeVal(elem)); if (!Number.isNaN(val)) { properties.push([alias, val]); } } return properties; } function coordPair(node) { const ll = [num1(node, "LongitudeDegrees"), num1(node, "LatitudeDegrees")]; if (ll[0] === undefined || Number.isNaN(ll[0]) || ll[1] === undefined || Number.isNaN(ll[1])) { return null; } const heartRate = get1(node, "HeartRateBpm"); const time = nodeVal(get1(node, "Time")); get1(node, "AltitudeMeters", (alt) => { const a = Number.parseFloat(nodeVal(alt)); if (!Number.isNaN(a)) { ll.push(a); } }); return { coordinates: ll, time: time || null, heartRate: heartRate ? Number.parseFloat(nodeVal(heartRate)) : null, extensions: getProperties(node, TRACKPOINT_ATTRIBUTES), }; } function getPoints(node) { const pts = $(node, "Trackpoint"); const line = []; const times = []; const heartRates = []; if (pts.length < 2) return null; // Invalid line in GeoJSON const extendedProperties = {}; const result = { extendedProperties }; for (let i = 0; i < pts.length; i++) { const c = coordPair(pts[i]); if (c === null) continue; line.push(c.coordinates); const { time, heartRate, extensions } = c; if (time) times.push(time); if (heartRate) heartRates.push(heartRate); for (const [alias, value] of extensions) { if (!extendedProperties[alias]) { extendedProperties[alias] = Array(pts.length).fill(null); } extendedProperties[alias][i] = value; } } if (line.length < 2) return null; return Object.assign(result, { line: line, times: times, heartRates: heartRates, }); } function getLap(node) { const segments = $(node, "Track"); const track = []; const times = []; const heartRates = []; const allExtendedProperties = []; let line; const properties = Object.assign(Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)), get(node, "Name", (nameElement) => { return { name: nodeVal(nameElement) }; })); for (const segment of segments) { line = getPoints(segment); if (line) { track.push(line.line); if (line.times.length) times.push(line.times); if (line.heartRates.length) heartRates.push(line.heartRates); allExtendedProperties.push(line.extendedProperties); } } for (let i = 0; i < allExtendedProperties.length; i++) { const extendedProperties = allExtendedProperties[i]; for (const property in extendedProperties) { if (segments.length === 1) { if (line) { properties[property] = line.extendedProperties[property]; } } else { if (!properties[property]) { properties[property] = track.map((track) => Array(track.length).fill(null)); } properties[property][i] = extendedProperties[property]; } } } if (track.length === 0) return null; if (times.length || heartRates.length) { properties.coordinateProperties = Object.assign(times.length ? { times: track.length === 1 ? times[0] : times, } : {}, heartRates.length ? { heart: track.length === 1 ? heartRates[0] : heartRates, } : {}); } return { type: "Feature", properties: properties, geometry: track.length === 1 ? { type: "LineString", coordinates: track[0], } : { type: "MultiLineString", coordinates: track, }, }; } /** * Incrementally convert a TCX document to GeoJSON. The * first argument, `doc`, must be a TCX * document as an XML DOM - not as a string. */ function* tcxGen(node) { for (const lap of $(node, "Lap")) { const feature = getLap(lap); if (feature) yield feature; } for (const course of $(node, "Courses")) { const feature = getLap(course); if (feature) yield feature; } } /** * Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX * document as an XML DOM - not as a string. */ function tcx(node) { return { type: "FeatureCollection", features: Array.from(tcxGen(node)), }; } function fixColor(v, prefix) { const properties = {}; const colorProp = prefix === "stroke" || prefix === "fill" ? prefix : `${prefix}-color`; if (v[0] === "#") { v = v.substring(1); } if (v.length === 6 || v.length === 3) { properties[colorProp] = `#${v}`; } else if (v.length === 8) { properties[`${prefix}-opacity`] = Number.parseInt(v.substring(0, 2), 16) / 255; properties[colorProp] = `#${v.substring(6, 8)}${v.substring(4, 6)}${v.substring(2, 4)}`; } return properties; } function numericProperty(node, source, target) { const properties = {}; num1(node, source, (val) => { properties[target] = val; }); return properties; } function getColor(node, output) { return get(node, "color", (elem) => fixColor(nodeVal(elem), output)); } function extractIconHref(node) { return get(node, "Icon", (icon, properties) => { val1(icon, "href", (href) => { properties.icon = href; }); return properties; }); } function extractIcon(node) { return get(node, "IconStyle", (iconStyle) => { return Object.assign(getColor(iconStyle, "icon"), numericProperty(iconStyle, "scale", "icon-scale"), numericProperty(iconStyle, "heading", "icon-heading"), get(iconStyle, "hotSpot", (hotspot) => { const left = Number.parseFloat(hotspot.getAttribute("x") || ""); const top = Number.parseFloat(hotspot.getAttribute("y") || ""); const xunits = hotspot.getAttribute("xunits") || ""; const yunits = hotspot.getAttribute("yunits") || ""; if (!Number.isNaN(left) && !Number.isNaN(top)) return { "icon-offset": [left, top], "icon-offset-units": [xunits, yunits], }; return {}; }), extractIconHref(iconStyle)); }); } function extractLabel(node) { return get(node, "LabelStyle", (labelStyle) => { return Object.assign(getColor(labelStyle, "label"), numericProperty(labelStyle, "scale", "label-scale")); }); } function extractLine(node) { return get(node, "LineStyle", (lineStyle) => { return Object.assign(getColor(lineStyle, "stroke"), numericProperty(lineStyle, "width", "stroke-width")); }); } function extractPoly(node) { return get(node, "PolyStyle", (polyStyle, properties) => { return Object.assign(properties, get(polyStyle, "color", (elem) => fixColor(nodeVal(elem), "fill")), val1(polyStyle, "fill", (fill) => { if (fill === "0") return { "fill-opacity": 0 }; }), val1(polyStyle, "outline", (outline) => { if (outline === "0") return { "stroke-opacity": 0 }; })); }); } function extractStyle(node) { return Object.assign({}, extractPoly(node), extractLine(node), extractLabel(node), extractIcon(node)); } const removeSpace = /\s*/g; const trimSpace = /^\s*|\s*$/g; const splitSpace = /\s+/; /** * Get one coordinate from a coordinate array, if any */ function coord1(value) { return value .replace(removeSpace, "") .split(",") .map(Number.parseFloat) .filter((num) => !Number.isNaN(num)) .slice(0, 3); } /** * Get all coordinates from a coordinate array as [[],[]] */ function coord(value) { return value .replace(trimSpace, "") .split(splitSpace) .map(coord1) .filter((coord) => { return coord.length >= 2; }); } function gxCoords(node) { let elems = $(node, "coord"); if (elems.length === 0) { elems = $ns(node, "coord", "*"); } const coordinates = elems.map((elem) => { return nodeVal(elem).split(" ").map(Number.parseFloat); }); if (coordinates.length === 0) { return null; } return { geometry: coordinates.length > 2 ? { type: "LineString", coordinates, } : { type: "Point", coordinates: coordinates[0], }, times: $(node, "when").map((elem) => nodeVal(elem)), }; } function fixRing(ring) { if (ring.length === 0) return ring; const first = ring[0]; const last = ring[ring.length - 1]; let equal = true; for (let i = 0; i < Math.max(first.length, last.length); i++) { if (first[i] !== last[i]) { equal = false; break; } } if (!equal) { return ring.concat([ring[0]]); } return ring; } function getCoordinates(node) { return nodeVal(get1(node, "coordinates")); } function getGeometry(node) { let geometries = []; let coordTimes = []; for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes.item(i); if (isElement(child)) { switch (child.tagName) { case "MultiGeometry": case "MultiTrack": case "gx:MultiTrack": { const childGeometries = getGeometry(child); geometries = geometries.concat(childGeometries.geometries); coordTimes = coordTimes.concat(childGeometries.coordTimes); break; } case "Point": { const coordinates = coord1(getCoordinates(child)); if (coordinates.length >= 2) { geometries.push({ type: "Point", coordinates, }); } break; } case "LinearRing": case "LineString": { const coordinates = coord(getCoordinates(child)); if (coordinates.length >= 2) { geometries.push({ type: "LineString", coordinates, }); } break; } case "Polygon": { const coords = []; for (const linearRing of $(child, "LinearRing")) { const ring = fixRing(coord(getCoordinates(linearRing))); if (ring.length >= 4) { coords.push(ring); } } if (coords.length) { geometries.push({ type: "Polygon", coordinates: coords, }); } break; } case "Track": case "gx:Track": { const gx = gxCoords(child); if (!gx) break; const { times, geometry } = gx; geometries.push(geometry); if (times.length) coordTimes.push(times); break; } } } } return { geometries, coordTimes, }; } const toNumber = (x) => Number(x); const typeConverters = { string: (x) => x, int: toNumber, uint: toNumber, short: toNumber, ushort: toNumber, float: toNumber, double: toNumber, bool: (x) => Boolean(x), }; function extractExtendedData(node, schema) { return get(node, "ExtendedData", (extendedData, properties) => { for (const data of $(extendedData, "Data")) { properties[data.getAttribute("name") || ""] = nodeVal(get1(data, "value")); } for (const simpleData of $(extendedData, "SimpleData")) { const name = simpleData.getAttribute("name") || ""; const typeConverter = schema[name] || typeConverters.string; properties[name] = typeConverter(nodeVal(simpleData)); } return properties; }); } function getMaybeHTMLDescription(node) { const descriptionNode = get1(node, "description"); for (const c of Array.from(descriptionNode?.childNodes || [])) { if (c.nodeType === 4) { return { description: { "@type": "html", value: nodeVal(c), }, }; } } return {}; } function extractTimeSpan(node) { return get(node, "TimeSpan", (timeSpan) => { return { timespan: { begin: nodeVal(get1(timeSpan, "begin")), end: nodeVal(get1(timeSpan, "end")), }, }; }); } function extractTimeStamp(node) { return get(node, "TimeStamp", (timeStamp) => { return { timestamp: nodeVal(get1(timeStamp, "when")) }; }); } function extractCascadedStyle(node, styleMap) { return val1(node, "styleUrl", (styleUrl) => { styleUrl = normalizeId(styleUrl); if (styleMap[styleUrl]) { return Object.assign({ styleUrl }, styleMap[styleUrl]); } // For backward-compatibility. Should we still include // styleUrl even if it's not resolved? return { styleUrl }; }); } function getGroundOverlayBox(node) { const latLonQuad = get1(node, "gx:LatLonQuad"); if (latLonQuad) { const ring = fixRing(coord(getCoordinates(node))); return { geometry: { type: "Polygon", coordinates: [ring], }, }; } return getLatLonBox(node); } const DEGREES_TO_RADIANS = Math.PI / 180; function rotateBox(bbox, coordinates, rotation) { const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; return [ coordinates[0].map((coordinate) => { const dy = coordinate[1] - center[1]; const dx = coordinate[0] - center[0]; const distance = Math.sqrt(dy ** 2 + dx ** 2); const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS; return [ center[0] + Math.cos(angle) * distance, center[1] + Math.sin(angle) * distance, ]; }), ]; } function getLatLonBox(node) { const latLonBox = get1(node, "LatLonBox"); if (latLonBox) { const north = num1(latLonBox, "north"); const west = num1(latLonBox, "west"); const east = num1(latLonBox, "east"); const south = num1(latLonBox, "south"); const rotation = num1(latLonBox, "rotation"); if (typeof north === "number" && typeof south === "number" && typeof west === "number" && typeof east === "number") { const bbox = [west, south, east, north]; let coordinates = [ [ [west, north], // top left [east, north], // top right [east, south], // top right [west, south], // bottom left [west, north], // top left (again) ], ]; if (typeof rotation === "number") { coordinates = rotateBox(bbox, coordinates, rotation); } return { bbox, geometry: { type: "Polygon", coordinates, }, }; } } return null; } function getGroundOverlay(node, styleMap, schema, options) { const box = getGroundOverlayBox(node); const geometry = box?.geometry || null; if (!geometry && options.skipNullGeometry) { return null; } const feature = { type: "Feature", geometry, properties: Object.assign( /** * Related to * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46 */ { "@geometry-type": "groundoverlay" }, getMulti(node, [ "name", "address", "visibility", "open", "phoneNumber", "description", ]), getMaybeHTMLDescription(node), extractCascadedStyle(node, styleMap), extractStyle(node), extractIconHref(node), extractExtendedData(node, schema), extractTimeSpan(node), extractTimeStamp(node)), }; if (box?.bbox) { feature.bbox = box.bbox; } if (feature.properties?.visibility !== undefined) { feature.properties.visibility = feature.properties.visibility !== "0"; } const id = node.getAttribute("id"); if (id !== null && id !== "") feature.id = id; return feature; } function geometryListToGeometry(geometries) { return geometries.length === 0 ? null : geometries.length === 1 ? geometries[0] : { type: "GeometryCollection", geometries, }; } function getPlacemark(node, styleMap, schema, options) { const { coordTimes, geometries } = getGeometry(node); const geometry = geometryListToGeometry(geometries); if (!geometry && options.skipNullGeometry) { return null; } const feature = { type: "Feature", geometry, properties: Object.assign(getMulti(node, [ "name", "address", "visibility", "open", "phoneNumber", "description", ]), getMaybeHTMLDescription(node), extractCascadedStyle(node, styleMap), extractStyle(node), extractExtendedData(node, schema), extractTimeSpan(node), extractTimeStamp(node), coordTimes.length ? { coordinateProperties: { times: coordTimes.length === 1 ? coordTimes[0] : coordTimes, }, } : {}), }; if (feature.properties?.visibility !== undefined) { feature.properties.visibility = feature.properties.visibility !== "0"; } const id = node.getAttribute("id"); if (id !== null && id !== "") feature.id = id; return feature; } function getStyleId(style) { let id = style.getAttribute("id"); const parentNode = style.parentNode; if (!id && isElement(parentNode) && parentNode.localName === "CascadingStyle") { id = parentNode.getAttribute("kml:id") || parentNode.getAttribute("id"); } return normalizeId(id || ""); } function buildStyleMap(node) { const styleMap = {}; for (const style of $(node, "Style")) { styleMap[getStyleId(style)] = extractStyle(style); } for (const map of $(node, "StyleMap")) { const id = normalizeId(map.getAttribute("id") || ""); val1(map, "styleUrl", (styleUrl) => { styleUrl = normalizeId(styleUrl); if (styleMap[styleUrl]) { styleMap[id] = styleMap[styleUrl]; } }); } return styleMap; } function buildSchema(node) { const schema = {}; for (const field of $(node, "SimpleField")) { schema[field.getAttribute("name") || ""] = typeConverters[field.getAttribute("type") || ""] || typeConverters.string; } return schema; } const FOLDER_PROPS = [ "name", "visibility", "open", "address", "description", "phoneNumber", "visibility", ]; function getFolder(node) { const meta = {}; for (const child of Array.from(node.childNodes)) { if (isElement(child) && FOLDER_PROPS.includes(child.tagName)) { meta[child.tagName] = nodeVal(child); } } return { type: "folder", meta, children: [], }; } /** * Yield a nested tree with KML folder structure * * This generates a tree with the given structure: * * ```js * { * "type": "root", * "children": [ * { * "type": "folder", * "meta": { * "name": "Test" * }, * "children": [ * // ...features and folders * ] * } * // ...features * ] * } * ``` * * ### GroundOverlay * * GroundOverlay elements are converted into * `Feature` objects with `Polygon` geometries, * a property like: * * ```json * { * "@geometry-type": "groundoverlay" * } * ``` * * And the ground overlay's image URL in the `href` * property. Ground overlays will need to be displayed * with a separate method to other features, depending * on which map framework you're using. */ function kmlWithFolders(node, options = { skipNullGeometry: false, }) { const n = node; const styleMap = buildStyleMap(n); const schema = buildSchema(n); const tree = { type: "root", children: [] }; function traverse(node, pointer, options) { if (isElement(node)) { switch (node.tagName) { case "GroundOverlay": { const placemark = getGroundOverlay(node, styleMap, schema, options); if (placemark) { pointer.children.push(placemark); } break; } case "Placemark": { const placemark = getPlacemark(node, styleMap, schema, options); if (placemark) { pointer.children.push(placemark); } break; } case "Folder": { const folder = getFolder(node); pointer.children.push(folder); pointer = folder; break; } } } if (node.childNodes) { for (let i = 0; i < node.childNodes.length; i++) { traverse(node.childNodes[i], pointer, options); } } } traverse(n, tree, options); return tree; } /** * Convert KML to GeoJSON incrementally, returning * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) * that yields output feature by feature. */ function* kmlGen(node, options = { skipNullGeometry: false, }) { const n = node; const styleMap = buildStyleMap(n); const schema = buildSchema(n); for (const placemark of $(n, "Placemark")) { const feature = getPlacemark(placemark, styleMap, schema, options); if (feature) yield feature; } for (const groundOverlay of $(n, "GroundOverlay")) { const feature = getGroundOverlay(groundOverlay, styleMap, schema, options); if (feature) yield feature; } } /** * Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML * document as an XML DOM - not as a string. You can get this using jQuery's default * `.ajax` function or using a bare XMLHttpRequest with the `.response` property * holding an XML DOM. * * The output is a JavaScript object of GeoJSON data. You can convert it to a string * with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) * or use it directly in libraries. */ function kml(node, options = { skipNullGeometry: false, }) { return { type: "FeatureCollection", features: Array.from(kmlGen(node, options)), }; } exports.gpx = gpx; exports.gpxGen = gpxGen; exports.kml = kml; exports.kmlGen = kmlGen; exports.kmlWithFolders = kmlWithFolders; exports.tcx = tcx; exports.tcxGen = tcxGen; //# sourceMappingURL=togeojson.cjs.map