Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve KML compatibility with Google Earth and fix a few bugs. #2539

Merged
merged 3 commits into from
Mar 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Change Log
==========

### 1.8 - 2015-04-01
* Breaking changes
*
* Deprecated
*
* Improved KML compatibility to work with non-specification compliant KML files that still happen to load in Google Earth.
* Fixed a crash when loading KML features that have no description and an empty `ExtendedData` node.
* Added support for KML `TimeStamp` nodes.

### 1.7 - 2015-03-02

* Breaking changes
Expand Down
78 changes: 58 additions & 20 deletions Source/DataSources/KmlDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ define([
function queryStringValue(node, tagName, namespace) {
var result = queryFirstNode(node, tagName, namespace);
if (defined(result)) {
return result.textContent;
return result.textContent.trim();
}
return undefined;
}
Expand Down Expand Up @@ -474,6 +474,28 @@ define([
return parseColorString(value, queryStringValue(node, 'colorMode', namespace) === 'random');
}

function processTimeStamp(featureNode) {
var node = queryFirstNode(featureNode, 'TimeStamp', namespaces.kmlgx);
var whenString = queryStringValue(node, 'when', namespaces.kmlgx);

if (!defined(node) || !defined(whenString) || whenString.length === 0) {
return undefined;
}

//According to the KML spec, a TimeStamp represents a "single moment in time"
//However, since Cesium animates much differently than Google Earth, that doesn't
//Make much sense here. Instead, we use the TimeStamp as the moment the feature
//comes into existence. This works much better and gives a similar feel to
//GE's experience.
var when = JulianDate.fromIso8601(whenString);
var result = new TimeIntervalCollection();
result.addInterval(new TimeInterval({
start : when,
stop : Iso8601.MAXIMUM_VALUE
}));
return result;
}

function processTimeSpan(featureNode) {
var node = queryFirstNode(featureNode, 'TimeSpan', namespaces.kmlgx);
if (!defined(node)) {
Expand Down Expand Up @@ -676,10 +698,17 @@ define([
}

//Google earth seems to always use the first external style only.
var externalStyle = queryFirstNode(placeMark, 'styleUrl', namespaces.kml);
var externalStyle = queryStringValue(placeMark, 'styleUrl', namespaces.kml);
if (defined(externalStyle)) {
var styleEntity = styleCollection.getById(externalStyle.textContent);
if (typeof styleEntity !== 'undefined') {
//Google Earth ignores leading and trailing whitespace for styleUrls
//Without the below trim, some docs that load in Google Earth won't load
//in cesium.
var id = externalStyle;
var styleEntity = styleCollection.getById(id);
if (!defined(styleEntity)) {
styleEntity = styleCollection.getById('#' + id);
}
if (defined(styleEntity)) {
result.merge(styleEntity);
}
}
Expand Down Expand Up @@ -769,18 +798,21 @@ define([
for (i = 0; i < styleUrlNodesLength; i++) {
var styleReference = styleUrlNodes[i].textContent;
if (styleReference[0] !== '#') {
//According to the spec, all local styles should start with a #
//and everything else is an external style that has a # seperating
//the URL of the document and the style. However, Google Earth
//also accepts styleUrls without a # as meaning a local style.
var tokens = styleReference.split('#');
if (tokens.length !== 2) {
window.console.log('KML - Invalid style reference: ' + styleReference);
}
var uri = tokens[0];
if (!defined(externalStyleHash[uri])) {
if (defined(sourceUri)) {
var baseUri = new Uri(document.location.href);
sourceUri = new Uri(sourceUri);
uri = new Uri(uri).resolve(sourceUri.resolve(baseUri)).toString();
if (tokens.length === 2) {
var uri = tokens[0];
if (!defined(externalStyleHash[uri])) {
if (defined(sourceUri)) {
var baseUri = new Uri(document.location.href);
sourceUri = new Uri(sourceUri);
uri = new Uri(uri).resolve(sourceUri.resolve(baseUri)).toString();
}
promises.push(processExternalStyles(dataSource, uri, styleCollection, sourceUri));
}
promises.push(processExternalStyles(dataSource, uri, styleCollection, sourceUri));
}
}
}
Expand Down Expand Up @@ -1171,10 +1203,6 @@ define([
text = defaultValue(balloonStyle.text, description);
}

if (!defined(text) && !defined(extendedData)) {
return;
}

var value;
if (defined(text)) {
text = text.replace('$[name]', defaultValue(entity.name, ''));
Expand Down Expand Up @@ -1207,7 +1235,7 @@ define([
}
}
}
} else {
} else if (defined(extendedData)) {
//If no description exists, build a table out of the extended data
keys = Object.keys(extendedData);
if (keys.length > 0) {
Expand All @@ -1221,6 +1249,11 @@ define([
}
}

if (!defined(text)) {
//No description
return;
}

//Turns non-explicit links into clickable links.
text = autolinker.link(text);

Expand Down Expand Up @@ -1259,7 +1292,12 @@ define([
var name = queryStringValue(featureNode, 'name', namespaces.kml);
entity.name = name;
entity.parent = parent;
entity.availability = processTimeSpan(featureNode);

var availability = processTimeSpan(featureNode);
if (!defined(availability)) {
availability = processTimeStamp(featureNode);
}
entity.availability = availability;

//var visibility = queryBooleanValue(featureNode, 'visibility', namespaces.kml);
//entity.uiShow = defaultValue(visibility, true);
Expand Down
78 changes: 78 additions & 0 deletions Specs/DataSources/KmlDataSourceSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,51 @@ defineSuite([
expect(entity.availability.stop).toEqual(Iso8601.MAXIMUM_VALUE);
});

it('Feature: TimeStamp works', function() {
var date = JulianDate.fromIso8601('1941-12-07');

var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
<TimeStamp>\
<when>1941-12-07</when>\
</TimeStamp>\
</Placemark>';

waitsForPromise(KmlDataSource.load(parser.parseFromString(kml, "text/xml")).then(function(dataSource) {
var entity = dataSource.entities.values[0];
expect(entity.availability).toBeDefined();
expect(entity.availability.start).toEqual(date);
expect(entity.availability.stop).toEqual(Iso8601.MAXIMUM_VALUE);
}));
});

it('Feature: TimeStamp gracefully handles empty fields', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
<TimeStamp>\
</TimeStamp>\
</Placemark>';

waitsForPromise(KmlDataSource.load(parser.parseFromString(kml, "text/xml")).then(function(dataSource) {
var entity = dataSource.entities.values[0];
expect(entity.availability).toBeUndefined();
}));
});

it('Feature: TimeStamp gracefully handles empty when field', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
<TimeStamp>\
<when></when>\
</TimeStamp>\
</Placemark>';

waitsForPromise(KmlDataSource.load(parser.parseFromString(kml, "text/xml")).then(function(dataSource) {
var entity = dataSource.entities.values[0];
expect(entity.availability).toBeUndefined();
}));
});

it('Feature: ExtendedData <Data> schema', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
Expand Down Expand Up @@ -702,6 +747,26 @@ defineSuite([
expect(entities[0].billboard.scale.getValue()).toEqual(3.0);
});

it('Styles: supports local styles with styleUrl mssing #', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Document>\
<Style id="testStyle">\
<IconStyle>\
<scale>3</scale>\
</IconStyle>\
</Style>\
<Placemark>\
<styleUrl>testStyle</styleUrl>\
</Placemark>\
</Document>';

waitsForPromise(KmlDataSource.load(parser.parseFromString(kml, "text/xml")).then(function(dataSource) {
var entities = dataSource.entities.values;
expect(entities.length).toEqual(1);
expect(entities[0].billboard.scale.getValue()).toEqual(3.0);
}));
});

it('Styles: supports external styles with styleUrl', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
Expand Down Expand Up @@ -1541,6 +1606,19 @@ defineSuite([
expect(table.rows[2].cells[1].textContent).toEqual('');
});

it('BalloonStyle: does not create a description for empty ExtendedData', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
<ExtendedData>\
</ExtendedData>\
</Placemark>';

waitsForPromise(KmlDataSource.load(parser.parseFromString(kml, "text/xml")).then(function(dataSource) {
var entity = dataSource.entities.values[0];
expect(entity.description).toBeUndefined();
}));
});

it('BalloonStyle: description creates links from text', function() {
var kml = '<?xml version="1.0" encoding="UTF-8"?>\
<Placemark>\
Expand Down