Saturday, 15 March 2008

The polygon hole problem in Google Earth

When I’m watching my 3D choropleth maps in Google Earth, strange "holes" appear in polygons representing countries with low values on a statistical indicator (e.g. having a low altitude value). The map below (KML) shows the digital divide in the world (number of Internet users):

The hole in India:


KML has three parameters for controlling the behaviour of polygons; extrude, tessellate and altitudeMode. By setting altitudeMode to clampToGround, my country polygons follow the great circle and get a solid fill. The problem arises when I extrude the polygons by adding an altitude representing a statistical value. Only the vertices of the polygon are extruded to the given altitude, and not the centre of geometry. I miss a clampToAltitude option in KML. A shape should follow earth’s profile, at the requested height above the surface.

There are some workaround to this problem:
  • Give all polygons a minimum altitude to support a "flat roof". Has to be a high value for a country like Russia.
  • Break up large polygons into smaller pieces.
  • Add additional clampToGround polygons to "hide" the holes. Only works with solid fills (no transparency).
Data source for Internet users: UNdata

3 comments:

Christopher Schmidt said...

Are you sure that settling tesslate to true doesn't solve this problem? I know that it does with lines along the surface.

Bjørn Sandvik said...

Hi,
The examples I've provided already have tesselation enabled. According to the KML 2.2 Reference, tesselation only works when the value of altitudeMode is clampToGround. That is why polygons without an altitude value are displayed properly.

Fabrizio said...

I tried a workaround setting the property to "clampToGround" for all those polygons having a value less than 10% of the maximum value.

see code

earth.getWindow().setVisibility(true);

var data = new google.visualization.DataTable(jsonData);

var options = {
type: 'prism',// 'choropleth',//
title: 'Call patterns',
maxHeight: 600000,
colorType: 'scale',
classification: 'equal',
geometry: worldBorders
};


var kml = map.draw(data, options);

kml = ''+kml;
kml = kml.replace('\\\\\\','');

if (window.DOMParser)
{
parser=new DOMParser();
xmlDoc=parser.parseFromString(kml,"text/xml");
}
else // Internet Explorer
{
xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async="false";
xmlDoc.loadXML(kml);
}

var root = xmlDoc.documentElement;

var placemarks = root.getElementsByTagName('Placemark');

// find the maximum value of the entire map
var maxValue = -1;

for (var i=0; i < placemarks.length; i++) {

var aPlaceMarkName = placemarks[i].getElementsByTagName('name')[0];

var myRegexp = /.+\s(\d+,\d+|\d+)/;
var match = myRegexp.exec(aPlaceMarkName.childNodes[0].nodeValue);

var curValue = parseInt(match[1].replace(',',''));

if(curValue>maxValue)
maxValue = curValue;

}


//change within the kml file, the altitudeMode tag of those elements having value less than 10% of the maximum

for (var i=0; i < placemarks.length; i++) {

var aPlaceMarkName = placemarks[i].getElementsByTagName('name')[0];

var myRegexp = /.+\s(\d+,\d+|\d+)/;
var match = myRegexp.exec(aPlaceMarkName.childNodes[0].nodeValue);

var curValue = parseInt(match[1].replace(',',''));

if (curValue < (maxValue/10) /*|| curValue<1000*/)
{
var altitudeModeValues = placemarks[i].getElementsByTagName('altitudeMode');

for (var j=0; j < altitudeModeValues.length; j++) {

altitudeModeValues[j].childNodes[0].nodeValue = 'clampToGround';

}

placemarks[i].getElementsByTagName('tessellate')[0].childNodes[0].nodeValue = 0;
}
}

if (window.DOMParser)
kmlString = (new XMLSerializer()).serializeToString(xmlDoc);
else
kmlString = xmlDoc.xml;

//document.write(kmlString);


var kmlObject = earth.parseKml(kmlString);