loaders.gl
Version:
Framework-independent loaders for 3D graphics formats
690 lines (604 loc) • 19.6 kB
JavaScript
/**
* Author - Nick Blackwell
* License - MIT
* Description - Defines a class, KMLParser which is a container for
* static kml parsing methods.
*/
/**
* KMLParser Class parses standard kml documents and returns objects
* representiong it's data.
* the optional transformations define the data within these objects,
* ie, documentTransform (for Geolive)
* will create a Layer object from its contents, and pull out the items
* which will be transformed aswell as MapItems.
*/
/* global console */
/* eslint-disable no-console */
export default class KMLParser {
constructor(xml) {
this.kml = xml;
}
parse() {
return {
documents: this._filter(KMLParser.ParseDomDocuments(this.kml)),
folders: this._filter(KMLParser.ParseDomFolders(this.kml)),
markers: this._filter(KMLParser.ParseDomMarkers(this.kml)),
polygons: this._filter(KMLParser.ParseDomPolygons(this.kml)),
lines: this._filter(KMLParser.ParseDomLines(this.kml)),
overlays: this._filter(KMLParser.ParseDomGroundOverlays(this.kml)),
links: this._filter(KMLParser.ParseDomLinks(this.kml))
};
}
parseDocuments(callback) {
const documentData = this._filter(KMLParser.ParseDomDocuments(this.kml));
documentData.forEach((p, i) => callback(p, this.kml, documentData, i));
return this;
}
parseFolders(callback) {
const folderData = this._filter(KMLParser.ParseDomFolders(this.kml));
folderData.forEach((p, i) => callback(p, this.kml, folderData, i));
return this;
}
parseMarkers(callback) {
const markerData = this._filter(KMLParser.ParseDomMarkers(this.kml));
markerData.forEach((p, i) => callback(p, this.kml, markerData, i));
return this;
}
parsePolygons(callback) {
const polygonData = this._filter(KMLParser.ParseDomPolygons(this.kml));
polygonData.forEach((p, i) => callback(p, this.kml, polygonData, i));
return this;
}
parseLines(callback) {
const lineData = this._filter(KMLParser.ParseDomLines(this.kml));
lineData.forEach((p, i) => callback(p, this.kml, lineData, i));
return this;
}
parseGroundOverlays(callback) {
const overlayData = this._filter(KMLParser.ParseDomGroundOverlays(this.kml));
overlayData.forEach((o, i) => callback(o, this.kml, overlayData, i));
return this;
}
parseNetworklinks(callback) {
const linkData = this._filter(KMLParser.ParseDomLinks(this.kml));
linkData.forEach((p, i) => callback(p, this.kml, linkData, i));
return this;
}
_filter(a) {
const filtered = [];
if (this._filters && a && a.length) {
a.forEach(item => {
let bool = true;
this._filters.forEach(f => {
if (f(item) === false) {
bool = false;
}
});
if (bool) {
filtered.push(item);
}
});
return filtered;
}
return a;
}
addFilter(filter) {
if (!this._filters) {
this._filters = [];
}
this._filters.push(filter);
return this;
}
static ParseDomDocuments(xmlDom) {
const docs = [];
const docsDomNodes = xmlDom.getElementsByTagName('Document');
for (let i = 0; i < docsDomNodes.length; i++) {
const node = docsDomNodes.item(i);
const docsData = Object.assign(
{},
KMLParser.ParseDomDoc(node),
KMLParser.ParseNonSpatialDomData(node, {})
);
const transform = options => options;
docs.push(transform(docsData));
}
return docs;
}
static ParseDomDoc(xmlDom) {
return {};
}
static ParseDomFolders(xmlDom) {
const folders = [];
const folderDomNodes = KMLParser.ParseDomItems(xmlDom, 'Folder');
for (let i = 0; i < folderDomNodes.length; i++) {
const node = folderDomNodes[i];
const folderData = Object.assign(
{
type: 'folder'
},
KMLParser.ParseDomFolder(node),
KMLParser.ParseNonSpatialDomData(node, {})
);
const transform = options => options;
folders.push(transform(folderData));
}
return folders;
}
static ParseDomLinks(xmlDom) {
const links = [];
const linkDomNodes = xmlDom.getElementsByTagName('NetworkLink');
for (let i = 0; i < linkDomNodes.length; i++) {
const node = linkDomNodes.item(i);
const linkData = Object.assign({},
KMLParser.ParseDomLink(node),
KMLParser.ParseNonSpatialDomData(node, {})
);
const transform = options => options;
links.push(transform(linkData));
}
return links;
}
static ParseDomFolder(xmlDom) {
return {};
}
static ParseDomLink(xmlDom) {
const urls = xmlDom.getElementsByTagName('href');
const link = {
type: 'link'
};
if (urls.length > 0) {
const url = urls.item(0);
link.url = KMLParser.Value(url);
}
return link;
}
static ParseDomLines(xmlDom) {
const lines = [];
const lineDomNodes = KMLParser.ParseDomItems(xmlDom, 'LineString');
for (let i = 0; i < lineDomNodes.length; i++) {
const node = lineDomNodes[i];
const polygonData = Object.assign(
{
type: 'line',
lineColor: '#FF000000', // black
lineWidth: 1,
polyColor: '#77000000', // black semitransparent,
coordinates: KMLParser.ParseDomCoordinates(node) // returns an array of GLatLngs
},
KMLParser.ParseNonSpatialDomData(node, {}),
KMLParser.ResolveDomStyle(KMLParser.ParseDomStyle(node), xmlDom)
);
const rgb = KMLParser.convertKMLColorToRGB(polygonData.lineColor);
polygonData.lineOpacity = rgb.opacity;
polygonData.lineColor = rgb.color;
lines.push(polygonData);
}
return lines;
}
static ParseDomGroundOverlays(xmlDom) {
const lines = [];
const lineDomNodes = KMLParser.ParseDomItems(xmlDom, 'GroundOverlay');
for (let i = 0; i < lineDomNodes.length; i++) {
const node = lineDomNodes[i];
const polygonData = Object.assign(
{
type: 'imageoverlay',
icon: KMLParser.ParseDomIcon(node),
bounds: KMLParser.ParseDomBounds(node)
},
KMLParser.ParseNonSpatialDomData(node, {})
);
lines.push(polygonData);
}
return lines;
}
static ParseDomPolygons(xmlDom) {
const polygons = [];
const polygonDomNodes = KMLParser.ParseDomItems(xmlDom, 'Polygon');
for (let i = 0; i < polygonDomNodes.length; i++) {
const node = polygonDomNodes[i];
const polygonData = Object.assign(
{
type: 'polygon',
fill: true,
lineColor: '#FF000000', // black
lineWidth: 1,
polyColor: '#77000000', // black semitransparent,
coordinates: KMLParser.ParseDomCoordinates(node) // returns an array of google.maps.LatLng
},
KMLParser.ParseNonSpatialDomData(node, {}),
KMLParser.ResolveDomStyle(KMLParser.ParseDomStyle(node), xmlDom)
);
const lineRGB = KMLParser.convertKMLColorToRGB(polygonData.lineColor);
polygonData.lineOpacity = lineRGB.opacity;
polygonData.lineColor = lineRGB.color;
const polyRGB = KMLParser.convertKMLColorToRGB(polygonData.polyColor);
polygonData.polyOpacity = (polygonData.fill) ? polyRGB.opacity : 0;
polygonData.polyColor = polyRGB.color;
polygons.push(polygonData);
}
return polygons;
}
static ParseDomMarkers(xmlDom) {
const markers = [];
const markerDomNodes = KMLParser.ParseDomItems(xmlDom, 'Point');
for (let i = 0; i < markerDomNodes.length; i++) {
const node = markerDomNodes[i];
const coords = KMLParser.ParseDomCoordinates(node);
const marker = Object.assign(
{
type: 'point',
coordinates: coords[0] // returns an array of google.maps.LatLng
},
KMLParser.ParseNonSpatialDomData(node, {})
);
let icon = KMLParser.ParseDomStyle(node);
if (icon.charAt(0) === '#') {
icon = KMLParser.ResolveDomStyle(icon, xmlDom).icon;
}
if (icon) {
// better to not have any hint of an icon (ie: icon:null) so default can be used by caller
marker.icon = icon;
}
markers.push(marker);
}
return markers;
}
static ParseDomCoordinates(xmlDom) {
const coordNodes = xmlDom.getElementsByTagName('coordinates');
if (!coordNodes.length) {
console.warn(['KMLParser. DOM Node did not contain coordinates!', {
node: xmlDom
}]);
return null;
}
const node = coordNodes.item(0);
let s = KMLParser.Value(node);
s = s.trim();
const coordStrings = s.split(' ');
const coordinates = [];
Object.values(coordStrings).forEach(coord => {
const c = coord.split(',');
if (c.length > 1) {
// JSConsole([c[1],c[0]]);
coordinates.push([c[1], c[0]]);
}
});
return coordinates;
}
/* eslint-disable max-statements */
static ParseDomBounds(xmlDom) {
const coordNodes = xmlDom.getElementsByTagName('LatLonBox');
if (!coordNodes.length) {
console.warn(['KMLParser. DOM Node did not contain coordinates!', {
node: xmlDom
}]);
return null;
}
const node = coordNodes.item(0);
const norths = node.getElementsByTagName('north');
const souths = node.getElementsByTagName('south');
const easts = node.getElementsByTagName('east');
const wests = node.getElementsByTagName('west');
let north = null;
let south = null;
let east = null;
let west = null;
if (!norths.length) {
console.warn(['KMLParser. DOM LatLngBox Node did not contain north!', {
node: xmlDom
}]);
} else {
north = parseFloat(KMLParser.Value(norths.item(0)));
}
if (!souths.length) {
console.warn(['KMLParser. DOM LatLngBox Node did not contain south!', {
node: xmlDom
}]);
} else {
south = parseFloat(KMLParser.Value(souths.item(0)));
}
if (!easts.length) {
console.warn(['KMLParser. DOM LatLngBox Node did not contain east!', {
node: xmlDom
}]);
} else {
east = parseFloat(KMLParser.Value(easts.item(0)));
}
if (!wests.length) {
console.warn(['KMLParser. DOM LatLngBox Node did not contain west!', {
node: xmlDom
}]);
} else {
west = parseFloat(KMLParser.Value(wests.item(0)));
}
return {
north,
south,
east,
west
};
}
/* eslint-enable max-statements */
static ParseNonSpatialDomData(xmlDom, options) {
const config = Object.assign({}, {
maxOffset: 2
}, options);
const data = {
name: '',
description: null,
tags: {}
};
const names = xmlDom.getElementsByTagName('name');
for (let i = 0; i < names.length; i++) {
if (KMLParser.WithinOffsetDom(xmlDom, names.item(i), config.maxOffset)) {
data.name = (KMLParser.Value(names.item(i)));
break;
}
}
const descriptions = xmlDom.getElementsByTagName('description');
for (let i = 0; i < descriptions.length; i++) {
if (KMLParser.WithinOffsetDom(xmlDom, descriptions.item(i), config.maxOffset)) {
data.description = (KMLParser.Value(descriptions.item(i)));
break;
}
}
if (xmlDom.hasAttribute('id')) {
data.id = parseInt(xmlDom.getAttribute('id'), 10);
}
const extendedDatas = xmlDom.getElementsByTagName('ExtendedData');
for (let i = 0; i < extendedDatas.length; i++) {
if (KMLParser.WithinOffsetDom(xmlDom, extendedDatas.item(i), config.maxOffset)) {
for (let j = 0; j < extendedDatas.item(i).childNodes.length; j++) {
const c = extendedDatas.item(i).childNodes.item(j);
const t = KMLParser.ParseTag(c);
if (t.name !== '#text') { // eslint-disable-line
data.tags[t.name] = t.value;
}
}
}
}
return data;
}
static ParseTag(xmlDom) {
const tags = {
name: null,
value: {}
};
switch (xmlDom.nodeName) {
case 'Data':
case 'data':
// TODO: add data tags...
break;
case 'ID':
tags.name = 'ID';
tags.value = KMLParser.Value(xmlDom);
break;
default:
tags.name = xmlDom.nodeName;
tags.value = KMLParser.Value(xmlDom);
break;
}
return tags;
}
static WithinOffsetDom(parent, child, max) {
let current = child.parentNode;
for (let i = 0; i < max; i++) {
if (current.nodeName === (typeof parent === 'string' ? parent : parent.nodeName)) {
return true;
}
current = current.parentNode;
}
console.error(['KMLParser. Could not find parent node within expected bounds.', {
parentNode: parent,
childNode: child,
bounds: max
}]);
return false;
}
static ParseDomStyle(xmlDom, options) {
const config = Object.assign({}, {
defaultStyle: 'default'
}, options);
const styles = xmlDom.getElementsByTagName('styleUrl');
let style = config.defaultStyle;
if (styles.length === 0) {
console.warn(['KMLParser. DOM Node did not contain styleUrl!', {
node: xmlDom,
options: config
}]);
} else {
const node = styles.item(0);
style = (KMLParser.Value(node));
}
return style;
}
static ParseDomIcon(xmlDom, options) {
const config = Object.assign({}, {
defaultIcon: false,
defaultScale: 1.0
}, options);
const icons = xmlDom.getElementsByTagName('Icon');
let icon = config.defaultStyle;
let scale = config.defaultScale;
if (icons.length === 0) {
console.warn(['KMLParser. DOM Node did not contain Icon!', {
node: xmlDom,
options: config
}]);
} else {
const node = icons.item(0);
const urls = node.getElementsByTagName('href');
if (urls.length === 0) {
console.warn(['KMLParser. DOM Icon Node did not contain href!', {
node: xmlDom,
options: config
}]);
} else {
const hrefNode = urls.item(0);
icon = (KMLParser.Value(hrefNode));
}
const scales = node.getElementsByTagName('viewBoundScale');
if (scales.length === 0) {
console.warn(['KMLParser. DOM Icon Node did not contain viewBoundScale!', {
node: xmlDom,
options: config
}]);
} else {
const scaleNode = scales.item(0);
scale = parseFloat(KMLParser.Value(scaleNode));
}
}
return {
url: icon,
scale
};
}
/* eslint-disable max-depth, max-statements */
static ResolveDomStyle(style, xmlDom) {
const data = {};
const name = (style.charAt(0) === '#' ? style.substring(1, style.length) : style);
const styles = xmlDom.getElementsByTagName('Style');
for (let i = 0; i < styles.length; i++) {
const node = styles.item(i);
const id = node.getAttribute('id');
if (id === name) {
const lineStyles = node.getElementsByTagName('LineStyle');
const polyStyles = node.getElementsByTagName('PolyStyle');
const iconStyles = node.getElementsByTagName('href');
if (lineStyles.length > 0) {
const lineStyle = lineStyles.item(0);
const colors = lineStyle.getElementsByTagName('color');
if (colors.length > 0) {
const color = colors.item(0);
data.lineColor = KMLParser.Value(color);
}
const widths = lineStyle.getElementsByTagName('width');
if (widths.length > 0) {
const width = widths.item(0);
data.lineWidth = KMLParser.Value(width);
}
}
if (polyStyles.length > 0) {
const polyStyle = polyStyles.item(0);
const colors = polyStyle.getElementsByTagName('color');
if (colors.length > 0) {
const color = colors.item(0);
data.polyColor = KMLParser.Value(color);
}
const outlines = polyStyle.getElementsByTagName('outline');
if (outlines.length > 0) {
const outline = outlines.item(0);
const o = KMLParser.Value(outline);
data.outline = Boolean(o);
}
}
if (iconStyles.length > 0) {
const iconStyle = iconStyles.item(0);
const icon = KMLParser.Value(iconStyle);
data.icon = icon;
}
}
}
return data;
}
/* eslint-enable max-depth, max-statements */
/* eslint-disable */
static ParseDomItems(xmlDom, tag) {
const tagName = tag || 'Point';
const items = [];
const markerDomNodes = xmlDom.getElementsByTagName(tagName);
for (let i = 0; i < markerDomNodes.length; i++) {
const node = markerDomNodes.item(i);
if (tag === 'GroundOverlay') {
items.push(node);
continue;
}
const parent = (node.parentNode.nodeName === 'Placemark' ?
node.parentNode :
(node.parentNode.parentNode.nodeName === 'Placemark' ?
node.parentNode.parentNode : null));
if (parent === null) {
console.error(['Failed to find ParentNode for Element - ' + tagName, {
node: xmlDom
}]);
// ();
} else {
items.push(parent);
}
}
return items;
}
// KML Color is defined similar to RGB except it is in the opposite order and starts with opacity,
// #OOBBGGRR
static convertKMLColorToRGB(colorString) {
const colorStr = colorString.replace('#', '');
while (colorStr.length < 6) {
colorStr = '0' + colorStr;
} // make sure line is dark!
while (colorStr.length < 8) {
colorStr = 'F' + colorStr;
} // make sure opacity is a large fraction
if (colorStr.length > 8) {
colorStr = colorStr.substring(0, 8);
}
const color = colorStr.substring(6, 8) + colorStr.substring(4, 6) + colorStr.substring(2, 4);
const opacity = ((parseInt(colorStr.substring(0, 2), 16)) * 1.000) / (parseInt('FF', 16));
const rgbVal = {
color: '#' + color,
opacity: opacity
}
return rgbVal;
}
static RGBColorToKML(rgb, opacity) {
const colorStr = rgb.replace('#', '');
while (colorStr.length < 6) {
colorStr = '0' + colorStr;
} //make sure line is dark!
if (colorStr.length > 6) {
colorStr = colorStr.substring(0, 6);
}
if ((opacity != null)) {
if (opacity >= 0.0 && opacity <= 1.0) {
const opacityNum = opacity;
} else if (parseInt(opacity) >= 0.0 && parseInt(opacity) <= 1.0) {
const opacityNum = parseInt(opacity);
}
}
if ((opacityNum === null)) {
const opacityNum = 1.0;
}
const opacityNum = (opacityNum * 255.0);
const opacityStr = opacityNum.toString(16);
const kmlStr =
opacityStr.substring(0, 2) + '' +
colorStr.substring(4, 6) +
colorStr.substring(2, 4) +
colorStr.substring(0, 2);
return kmlStr;
}
static Value(node) {
const value = node.nodeValue;
if (value) return value;
let str = '';
try {
if (node.childNodes && node.childNodes.length) {
Object.values(KMLParser.ChildNodesArray(node)).forEach(c => {
str += KMLParser.Value(c);
});
}
} catch (e) {
console.error(['SimpleKML Parser Exception', e]);
}
return str;
}
static ChildNodesArray(node) {
const array = [];
if (node.childNodes && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
array.push(node.childNodes.item(i));
}
}
return array;
}
}