Browse Source

add align html elements to 3D article

Gregg Tavares 6 years ago
parent
commit
c86010c744

BIN
threejs/lessons/resources/images/overlapping-labels.png


+ 677 - 0
threejs/lessons/threejs-align-html-elements-to-3d.md

@@ -0,0 +1,677 @@
+Title: Three.js Aligning HTML Elements to 3D
+Description: How to line up an HTML Element to match a point in 3D space
+
+This article is part of a series of articles about three.js. The first article
+is [three.js fundamentals](threejs-fundamentals.html). If you haven't read that
+yet and you're new to three.js you might want to consider starting there. 
+
+Sometimes you'd like to display some text in your 3D scene. You have many options
+each with pluses and minuses.
+
+* Use 3D text
+
+  If you look at the [primitives article](threejs-primitives.html) you'll see `TextBufferGeometry` which
+  makes 3D text. This might be useful for flying logos but probably not so useful for stats, info,
+  or labelling lots of things.
+
+* Use a texture with 2D text drawn into it.
+
+  The article on [using a Canvas as a texture](threejs-textures-canvas.html) shows using
+  a canvas as a texture. You can draw text into a canvas and [display it as a billboard](threejs-billboards.html).
+  The plus here might be that the text is integrated into the 3D scene. For something like a computer terminal
+  shown in a 3D scene this might be perfect.
+
+* Use HTML Elements and position them to match the 3D
+
+  The benefits to this approach is you can use all of HTML. Your HTML can have multiple elements. It can
+  by styled with CSS. It can also be selected by the user as it is actual text. 
+
+This article will cover this last approach.
+
+Let's start simple. We'll make a 3D scene with a few primitives and then add a label to each primitive. We'll start
+with an example from [the article on responsive pages](threejs-responsive.html) 
+
+We'll add some `OrbitControls` like we did in [the article on lighting](threejs-lights.html).
+
+```html
+<script src="resources/threejs/r102/three.min.js"></script>
++<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+```
+
+```js
+const controls = new THREE.OrbitControls(camera, canvas);
+controls.target.set(0, 0, 0);
+controls.update();
+```
+
+We need to provide an HTML element to contain our label elements
+
+```html
+<body>
+-  <canvas id="c"></canvas>
++  <div id="container">
++    <canvas id="c"></canvas>
++    <div id="labels"></div>
++  </div>
+</body>
+```
+
+By putting both the canvas and the `<div id="labels">` inside a
+parent container we can make them overlap with this CSS
+
+```css
+#c {
+-    width: 100vw;
+-    height: 100vh;
++    width: 100%;  /* let our container decide our size */
++    height: 100%;
+    display: block;
+}
++#container {
++  position: relative;  /* makes this the origin of its children */
++  width: 100vw;
++  height: 100vh;
++  overflow: hidden;
++}
++#labels {
++  position: absolute;  /* let us position ourself inside the container */
++  left: 0;             /* make our position the top left of the container */
++  top: 0;
++  color: white;
++}
+```
+
+let's also add some CSS for the labels themselves
+
+```css
+#labels>div {
+  position: absolute;  /* let us position them inside the container */
+  left: 0;             /* make their default position the top left of the container */
+  top: 0;
+  cursor: pointer;     /* change the cursor to a hand when over us */
+  font-size: large;
+  user-select: none;   /* don't let the text get selected */
+  text-shadow:         /* create a black outline */
+    -1px -1px 0 #000,
+     0   -1px 0 #000,
+     1px -1px 0 #000,
+     1px  0   0 #000,
+     1px  1px 0 #000,
+     0    1px 0 #000,
+    -1px  1px 0 #000,
+    -1px  0   0 #000;
+}
+#labels>div:hover {
+  color: red;
+}
+```
+
+Now into our code we don't have to add too much. We had a function
+`makeInstance` that we used to generate cubes. Let's make it
+so we it also adds a label element.
+
+```js
++const labelContainerElem = document.querySelector('#labels');
+
+-function makeInstance(geometry, color, x) {
++function makeInstance(geometry, color, x, name) {
+  const material = new THREE.MeshPhongMaterial({color});
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+
+  cube.position.x = x;
+
++  const elem = document.createElement('div');
++  elem.textContent = name;
++  labelContainerElem.appendChild(elem);
+
+-  return cube;
++  return {cube, elem};
+}
+```
+
+As you can see we're adding a `<div>` to the container, one for each cube. We're
+also returning an object with both the `cube` and the `elem` for the label.
+
+Calling it we need to provide a name for each
+
+```js
+const cubes = [
+-  makeInstance(geometry, 0x44aa88,  0),
+-  makeInstance(geometry, 0x8844aa, -2),
+-  makeInstance(geometry, 0xaa8844,  2),
++  makeInstance(geometry, 0x44aa88,  0, 'Aqua'),
++  makeInstance(geometry, 0x8844aa, -2, 'Purple'),
++  makeInstance(geometry, 0xaa8844,  2, 'Gold'),
+];
+```
+
+What remains is positioning the label elements at render time
+
+```js
+const tempV = new THREE.Vector3();
+
+...
+
+-cubes.forEach((cube, ndx) => {
++cubes.forEach((cubeInfo, ndx) => {
++  const {cube, elem} = cubeInfo;
+  const speed = 1 + ndx * .1;
+  const rot = time * speed;
+  cube.rotation.x = rot;
+  cube.rotation.y = rot;
+
++  // get the position of the center of the cube
++  cube.updateWorldMatrix(true, false);
++  cube.getWorldPosition(tempV);
++
++  // get the normalized screen coordinate of that position
++  // x and y will be in the -1 to +1 range with x = -1 being
++  // on the left and y = -1 being on the bottom
++  tempV.project(camera);
++
++  // convert the normalized position to CSS coordinates
++  const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
++  const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
++
++  // move the elem to that position
++  elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+});
+```
+
+And with that we have labels aligned to their corresponding objects.
+
+{{{example url="../threejs-align-html-to-3d.html" }}}
+
+There are a couple of issues we probably want to deal with.
+
+One is that if we rotate the objects so they overlap all the labels
+overlap as well.
+
+<img src="resources/images/overlapping-labels.png" class="threejs_center" style="width: 307px;">
+
+Another is that if we zoom way out so that the objects go outside
+the frustum the labels will still appear.
+
+A possible solution to the problem of overlapping objects is to use
+the [picking code from the article on picking](threejs-picking.html).
+We'll pass in the position of the object on the screen and then
+ask the `RayCaster` to tell us which objects were intersected.
+If our object is not the first one then we are not in the front.
+
+```js
+const tempV = new THREE.Vector3();
++const raycaster = new THREE.Raycaster();
+
+...
+
+cubes.forEach((cubeInfo, ndx) => {
+  const {cube, elem} = cubeInfo;
+  const speed = 1 + ndx * .1;
+  const rot = time * speed;
+  cube.rotation.x = rot;
+  cube.rotation.y = rot;
+
+  // get the position of the center of the cube
+  cube.updateWorldMatrix(true, false);
+  cube.getWorldPosition(tempV);
+
+  // get the normalized screen coordinate of that position
+  // x and y will be in the -1 to +1 range with x = -1 being
+  // on the left and y = -1 being on the bottom
+  tempV.project(camera);
+
++  // ask the raycaster for all the objects that intersect
++  // from the eye toward this object's position
++  raycaster.setFromCamera(tempV, camera);
++  const intersectedObjects = raycaster.intersectObjects(scene.children);
++  // We're visible if the first intersection is this object.
++  const show = intersectedObjects.length && cube === intersectedObjects[0].object;
++
++  if (!show) {
++    // hide the label
++    elem.style.display = 'none';
++  } else {
++    // unhide the label
++    elem.style.display = '';
+
+    // convert the normalized position to CSS coordinates
+    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+    // move the elem to that position
+    elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
++  }
+});
+```
+
+This handles overlapping.
+
+To handle going outside the frustum we can add this check if the origin of
+the object is outside the frustum by checking `tempV.z`
+
+```js
+-  if (!show) {
++  if (!show || Math.abs(tempV.z) > 1) {
+    // hide the label
+    elem.style.display = 'none';
+```
+
+This *kind of* works because the normalized coordinates we computed include a `z`
+value that goes from -1 when at the `near` part of our camera frustum to +1 when
+at the `far` part of our camera frustum.
+
+{{{example url="../threejs-align-html-to-3d-w-hiding.html" }}}
+
+For the frustum check, the solution above fails as we're only checking the origin of the object. For a large
+object. That origin might go outside the frustum but half of the object might still be in the frustum.
+
+A more correct solution would be to check if the object itself is in the frustum
+or not. Unfortunate that check is slow. For 3 cubes it will not be a problem
+but for many objects it might be.
+
+Three.js provides some functions to check if an object's bounding sphere is
+in a frustum
+
+```js
+// at init time
+const frustum = new THREE.Frustum();
+const viewProjection = new THREE.Matrix4();
+
+...
+
+// before checking
+camera.updateMatrix();
+camera.updateMatrixWorld();
+camera.matrixWorldInverse.getInverse(camera.matrixWorld);
+
+...
+
+// then for each mesh
+someMesh.updateMatrix();
+someMesh.updateMatrixWorld();
+
+viewProjection.multiplyMatrices(
+    camera.projectionMatrix, camera.matrixWorldInverse);
+frustum.setFromMatrix(viewProjection);
+const inFrustum = frustum.contains(someMesh));
+```
+
+Our current overlapping solution has similar issues. Picking is slow. We could
+use gpu based picking like we covered in the [picking
+article](threejs-picking.html) but that is also not free. Which solution you
+chose depends on your needs.
+
+While we're at it let's do one more example to show one more issue.
+Let's draw a globe like Google Maps and label the countries.
+
+I found [this data](http://thematicmapping.org/downloads/world_borders.php)
+which contains the borders of countries. It's licensed as
+[CC-BY-SA](http://creativecommons.org/licenses/by-sa/3.0/).
+
+I [wrote some code](https://github.com/greggman/threejsfundamentals/blob/master/threejs/lessons/tools/geo-picking/)
+to load the data, and generate country outlines and some JSON data with the names
+of the countries and their locations.
+
+<img src="../resources/data/world/country-outlines-4k.png" style="background: black; width: 700px">
+
+The JSON data is an array of entries something like this
+
+```json
+[
+  {
+    "name": "Algeria",
+    "min": [
+      -8.667223,
+      18.976387
+    ],
+    "max": [
+      11.986475,
+      37.091385
+    ],
+    "area": 238174,
+    "lat": 28.163,
+    "lon": 2.632,
+    "population": {
+      "2005": 32854159
+    }
+  },
+  ...
+```
+
+where min, max, lat, lon, are all in latitude and longitude degrees.
+
+Let's load it up. The code is based on the examples from [optimizing lots of
+objects](threejs-optimize-lots-of-objects.html) though we are not drawing lots
+of objects we'll be using the same solutions for [rendering on
+demand](threejs-rendering-on-demand.htm).
+
+The first thing is to make a sphere and use the outline texture.
+
+```js
+{
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
+  const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+  const material = new THREE.MeshBasicMaterial({map: texture});
+  scene.add(new THREE.Mesh(geometry, material));
+}
+```
+
+Then let's load the JSON file by first making a loader
+
+```js
+async function loadJSON(url) {
+  const req = await fetch(url);
+  return req.json();
+}
+```
+
+and then calling it
+
+```js
+let countryInfos;
+async function loadCountryData() {
+  countryInfos = await loadJSON('resources/data/world/country-info.json');
+     ...
+  }
+  requestRenderIfNotRequested();
+}
+loadCountryData();
+```
+
+Now let's use that data to generate and place the labels.
+
+In the article on [optimizing lots of objects](threejs-optimize-lots-of-objects.html)
+we had setup a small scene graph of helper objects to make it easy to 
+compute latitude and longitude positions on our globe. See that article 
+for an explanation of how they work.
+
+```js
+const lonFudge = Math.PI * 1.5;
+const latFudge = Math.PI;
+// these helpers will make it easy to position the boxes
+// We can rotate the lon helper on its Y axis to the longitude
+const lonHelper = new THREE.Object3D();
+// We rotate the latHelper on its X axis to the latitude
+const latHelper = new THREE.Object3D();
+lonHelper.add(latHelper);
+// The position helper moves the object to the edge of the sphere
+const positionHelper = new THREE.Object3D();
+positionHelper.position.z = 1;
+latHelper.add(positionHelper);
+```
+
+We'll use that to compute a position for each label
+
+```js
+const labelParentElem = document.querySelector('#labels');
+for (const countryInfo of countryInfos) {
+  const {lat, lon, name} = countryInfo;
+
+  // adjust the helpers to point to the latitude and longitude
+  lonHelper.rotation.y = THREE.Math.degToRad(lon) + lonFudge;
+  latHelper.rotation.x = THREE.Math.degToRad(lat) + latFudge;
+
+  // get the position of the lat/lon
+  positionHelper.updateWorldMatrix(true, false);
+  const position = new THREE.Vector3();
+  positionHelper.getWorldPosition(position);
+  countryInfo.position = position;
+
+  // add an element for each country
+  const elem = document.createElement('div');
+  elem.textContent = name;
+  labelParentElem.appendChild(elem);
+  countryInfo.elem = elem;
+```
+
+The code above looks very similar to the code we wrote for making cube labels
+making an element per label. When we're done we have an array, `countryInfos`,
+with one entry for each country to which we've added an `elem` property for
+the label element for that country and a `position` with its position on the
+globe.
+
+Just like we did for the cubes we need to update the position of the
+labels and render time.
+
+```js
+const tempV = new THREE.Vector3();
+
+function updateLabels() {
+  // exit if we have not yet loaded the JSON file
+  if (!countryInfos) {
+    return;
+  }
+
+  for (const countryInfo of countryInfos) {
+    const {position, elem} = countryInfo;
+
+    // get the normalized screen coordinate of that position
+    // x and y will be in the -1 to +1 range with x = -1 being
+    // on the left and y = -1 being on the bottom
+    tempV.copy(position);
+    tempV.project(camera);
+
+    // convert the normalized position to CSS coordinates
+    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+    // move the elem to that position
+    elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+  }
+}
+```
+
+You can see the code above is substantially similar to the cube example before.
+The only major difference is we pre-computed the label positions at init time.
+We can do this because the globe never moves. Only our camera moves.
+
+Lastly we need to call `updateLabels` in our render loop
+
+```js
+function render() {
+  renderRequested = undefined;
+
+  if (resizeRendererToDisplaySize(renderer)) {
+    const canvas = renderer.domElement;
+    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.updateProjectionMatrix();
+  }
+
+  controls.update();
+
++  updateLabels();
+
+  renderer.render(scene, camera);
+}
+```
+
+And this is what we get
+
+{{{example url="../threejs-align-html-elements-to-3d-globe-too-many-labels.html" }}}
+
+That is way too many labels!
+
+We have 2 problems.
+
+1. Labels facing away from us are showing up.
+
+2. There are too many labels.
+
+For issue #1 we can't really use the `RayCaster` like we did above as there is
+nothing to intersect except the sphere. Instead what we can do is check if that
+particular country is facing away from us or not. This works because the label
+positions are around a sphere. In fact we're using a unit sphere, a sphere with
+a radius of 1.0. That means the positions are already unit directions making
+the math relatively easy.
+
+```js
+const tempV = new THREE.Vector3();
++const normalMatrix = new THREE.Matrix3();
++const positiveZ = new THREE.Vector3(0, 0, 1);
+
+function updateLabels() {
+  // exit if we have not yet loaded the JSON file
+  if (!countryInfos) {
+    return;
+  }
+
++  const visibleDot = Math.cos(THREE.Math.degToRad(75));
++  // get a matrix that represents a relative orientation of the camera
++  normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
+  for (const countryInfo of countryInfos) {
+    const {position, elem} = countryInfo;
+
++    // orient the position based on the camera's orientation
++    tempV.copy(position);
++    tempV.applyMatrix3(normalMatrix);
++
++    // get the dot product with positiveZ
++    // -1 = facing directly away and +1 = facing directly toward us
++    const dot = tempV.dot(positiveZ);
++
++    // if the orientation is not facing us hide it.
++    if (dot < visibleDot) {
++      elem.style.display = 'none';
++      continue;
++    }
++
++    // restore the element to its default display style
++    elem.style.display = '';
+
+    // get the normalized screen coordinate of that position
+    // x and y will be in the -1 to +1 range with x = -1 being
+    // on the left and y = -1 being on the bottom
+    tempV.copy(position);
+    tempV.project(camera);
+
+    // convert the normalized position to CSS coordinates
+    const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+    const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+    // move the elem to that position
+    countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+  }
+}
+```
+
+Above we use the positions as a direction, get their direction relative 
+to the camera and take the *dot product* with a positive Z vector. This
+gives us a value from -1 to 1 where -1 means the label is directly on the
+other side of the sphere and +1 means the label is directly on our side
+of the sphere. We then use that value to show or hide the element.
+
+For issue #2, too many labels we need some way to decide which labels
+to show. One way would be to only show labels for large countries.
+The data we're loading contains min and max values for the area a
+country covers. From that we can compute an area and then use that
+area to decide whether or not to display the country.
+
+At init time let's compute the area
+
+```js
+const labelParentElem = document.querySelector('#labels');
+for (const countryInfo of countryInfos) {
+  const {lat, lon, min, max, name} = countryInfo;
+
+  // adjust the helpers to point to the latitude and longitude
+  lonHelper.rotation.y = THREE.Math.degToRad(lon) + lonFudge;
+  latHelper.rotation.x = THREE.Math.degToRad(lat) + latFudge;
+
+  // get the position of the lat/lon
+  positionHelper.updateWorldMatrix(true, false);
+  const position = new THREE.Vector3();
+  positionHelper.getWorldPosition(position);
+  countryInfo.position = position;
+
++  // compute the area for each country
++  const width = max[0] - min[0];
++  const height = max[1] - min[1];
++  const area = width * height;
++  countryInfo.area = area;
+
+  // add an element for each country
+  const elem = document.createElement('div');
+  elem.textContent = name;
+  labelParentElem.appendChild(elem);
+  countryInfo.elem = elem;
+}
+```
+
+Then at render time let's use the area to decide to display the label
+or not
+
+```js
++const large = 20 * 20;
+const visibleDot = Math.cos(THREE.Math.degToRad(75));
+// get a matrix that represents a relative orientation of the camera
+normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
+for (const countryInfo of countryInfos) {
+-  const {position, elem} = countryInfo;
++  const {position, elem, area} = countryInfo;
++  // large enough?
++  if (area < large) {
++    elem.style.display = 'none';
++    continue;
++  }
+
+  ...
+```
+
+Finally, since I'm not sure what good values are for these settings lets
+add a GUI so we can play with them
+
+```html
+<script src="resources/threejs/r102/three.js"></script>
+<script src="resources/threejs/r102/js/utils/BufferGeometryUtils.js"></script>
+<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
++<script src="../3rdparty/dat.gui.min.js"></script>
+```
+
+```js
++const settings = {
++  minArea: 20,
++  visibleAngleDeg: 75,
++};
++const gui = new dat.GUI({width: 300});
++gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested);
++gui.add(settings, 'visibleAngleDeg', 0, 180).onChange(requestRenderIfNotRequested);
+
+function updateLabels() {
+  if (!countryInfos) {
+    return;
+  }
+
+-  const large = 20 * 20;
+-  const visibleDot = Math.cos(THREE.Math.degToRad(75));
++  const large = settings.minArea * settings.minArea;
++  const visibleDot = Math.cos(THREE.Math.degToRad(settings.visibleAngleDeg));
+  // get a matrix that represents a relative orientation of the camera
+  normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
+  for (const countryInfo of countryInfos) {
+
+    ...
+```
+
+and here's the result
+
+{{{example url="../threejs-align-html-elements-to-3d-globe.html" }}}
+
+You can see as you rotate the earth labels that go behind disappear.
+Adjust the `visibleAngleDeg` to see the cutoff change.
+
+You can also adjust  the `minArea` value to see larger or smaller countries
+appear.
+
+I'll be honest, the more I worked on this the more I realized just how much
+work is put into Google Maps. They have also have to decide which labels to
+show. I'm pretty sure they use all kinds of criteria. For example your current
+location, your default language setting, your account settings if you have an
+account.
+
+In any case I hope these examples gave you some idea of how to align HTML
+elements with your 3D.
+
+Next up let's make it so you can pick and highlight a country.

+ 6 - 0
threejs/lessons/threejs-billboards.md

@@ -0,0 +1,6 @@
+Title: Three.js Billboards
+Description: How to make things always face the camera.
+
+TBD
+
+

+ 6 - 0
threejs/lessons/threejs-textures-canvas.md

@@ -0,0 +1,6 @@
+Title: Three.js Canvas Textures
+Description: How to use a canvas as a texture.
+
+TBD
+
+

+ 193 - 0
threejs/lessons/tools/geo-picking/make-geo-picking-texture-ogc.html

@@ -0,0 +1,193 @@
+<canvas></canvas>
+<script src="ogc-parser.js"></script>
+<script>
+function wait(ms = 0) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+// # need to draw to 2nd canvas, then shave off non perfect pixels
+
+
+async function main() {
+  const ctx = document.querySelector('canvas').getContext('2d');
+  ctx.canvas.width = 2048;
+  ctx.canvas.height = 2048;
+  ctx.fillStyle = '#444';
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
+  ctx.scale(ctx.canvas.width / 360, ctx.canvas.height / -180);
+
+  function setColor(color) {
+    ctx.fillStyle = color;
+    ctx.strokeStyle = color;
+  }
+
+  function setEraseColor() {
+    setColor('#000');
+  }
+
+  const handlers = {
+    point,
+    lineString,
+    polygon,
+    multiPoint,
+    multiLineString,
+    multiPolygon,
+  };
+
+  function point(d) {
+    ctx.fillRect(...d.point, 1, 1);
+  }
+
+  function setPathFromPoints(points, backward = false) {
+    if (backward) {
+      const numPoints = points.length / 2;
+      const lastPoint = numPoints - 1;
+      ctx.moveTo(...points.slice(lastPoint * 2, lastPoint * 2 + 2));
+      for (let i = lastPoint - 1; i >= 0; i -= 2) {
+        ctx.lineTo(...points.slice(i * 2, i * 2 + 2));
+      }
+    } else {
+      ctx.moveTo(...points.slice(0, 2));
+      for (let i = 2; i < points.length; i += 2) {
+        ctx.lineTo(...points.slice(i, i + 2));
+      }
+    }
+  }
+
+  function stroke(ctx) {
+    ctx.save();
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  function lineString(d) {
+    ctx.beginPath();
+    setPathFromPoints(d.points);
+    stroke(ctx);
+  }
+
+  function polygon(d) {
+    ctx.beginPath();
+    d.rings.forEach((ring, ndx) => {
+      setPathFromPoints(ring, ndx !== 0);
+      ctx.closePath();
+    });
+    ctx.fill();
+    stroke(ctx);
+  }
+
+  function multiPoint(d) {
+    for (let i = 0; i < d.points.length; i += 2) {
+      ctx.fillRect(...d.points.slice(i, i + 2));
+    }
+  }
+
+  function multiLineString(d) {
+    d.lineStrings.forEach((lineString) => {
+      ctx.beginPath();
+      setPathFromPoints(lineString);
+      stroke(ctx);
+    });
+  }
+
+  function multiPolygon(d) {
+    d.polygons.forEach((polygon) => {
+      ctx.beginPath();
+      polygon.forEach((ring, ndx) => {
+        setPathFromPoints(ring, ndx !== 0);
+      });
+      ctx.fill();
+      stroke(ctx);
+    });
+  }
+
+  const colors = {};
+
+  //const req = await fetch('level1.json');
+  const req = await fetch('foo.json');
+  const areas = await req.json();
+  let min = [Number.MAX_VALUE, Number.MAX_VALUE];
+  let max = [Number.MIN_VALUE, Number.MIN_VALUE];
+  console.log('num areas:', areas.length);
+  for (let ndx = 0; ndx < areas.length; ++ndx) {
+    const area = areas[ndx];
+    try {
+      const buf = new Uint8Array(base64ToUint8Array(area.geom));
+      area.geom = ogcParser.parse(buf);
+    } catch (e) {
+      console.log('ERROR:', e);
+      console.log(JSON.stringify(area, null, 2));
+      throw e;
+    }
+
+    if (!colors[area.NAME_0]) {
+      colors[area.NAME_0] = rgb(r(), r(), r());
+    }
+
+    const color = colors[area.NAME_0];
+
+    console.log(ndx, area.NAME_0);
+
+    area.geom.primitives.forEach((primitive) => {
+      const fn = handlers[primitive.type];
+      setColor(color);
+      fn(primitive);
+    });
+
+    min[0] = Math.min(min[0], area.geom.envelope[0]);
+    min[0] = Math.min(min[0], area.geom.envelope[1]);
+    min[1] = Math.min(min[1], area.geom.envelope[2]);
+    min[1] = Math.min(min[1], area.geom.envelope[3]);
+
+    max[0] = Math.max(max[0], area.geom.envelope[0]);
+    max[0] = Math.max(max[0], area.geom.envelope[1]);
+    max[1] = Math.max(max[1], area.geom.envelope[2]);
+    max[1] = Math.max(max[1], area.geom.envelope[3]);
+
+    if (ndx % 100 === 99) {
+      await wait();
+    }
+  }
+
+  console.log('min', min);
+  console.log('max', max);
+}
+
+function r(min, max) {
+  if (min === undefined) {
+    min = 0;
+    max = 1;
+  } else if (max === undefined){
+    max = min;
+    min = 0;
+  }
+  return min + Math.random() * (max - min);
+}
+
+function rgb(r, g, b) {
+  return `rgb(${r * 255 | 0},${g * 255 | 0},${b * 255 | 0})`;
+}
+
+function hsl(h, s, l) {
+  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+}
+
+function base64ToUint8Array(base64) {
+  const raw = window.atob(base64);
+  const rawLength = raw.length;
+  const array = new Uint8Array(new ArrayBuffer(rawLength));
+
+  for(let i = 0; i < rawLength; ++i) {
+    array[i] = raw.charCodeAt(i);
+  }
+
+  return array;
+}
+
+main();
+
+</script>

+ 28 - 0
threejs/lessons/tools/geo-picking/make-geo-picking-texture-ogc.js

@@ -0,0 +1,28 @@
+const fs = require('fs');
+const path = require('path');
+const ogcParser = require('./ogc-parser');
+
+const baseDir = process.argv[2];
+
+function readJSON(name) {
+  return JSON.parse(fs.readFileSync(path.join(baseDir, name), {encoding: 'utf-8'}));
+}
+
+function main() {
+  const areas = readJSON('level1.json');
+  areas.forEach((area, ndx) => {
+    console.log(ndx);
+    try {
+      const buf = new Uint8Array(Buffer.from(area.geom, 'base64'));
+      area.geom = parseGeom(buf);
+    } catch (e) {
+      console.log('ERROR:', e);
+      console.log(JSON.stringify(area, null, 2));
+      throw e;
+    }
+  });
+
+  console.log(JSON.stringify(areas, null, 2));
+}
+
+main();

+ 17 - 0
threejs/lessons/tools/geo-picking/make-geo-picking-texture.html

@@ -0,0 +1,17 @@
+<html>
+  <style>
+  body {
+      background: #444;
+  }
+  canvas {
+      image-rendering: pixelated;
+  }
+  </style>
+  <body>
+    <canvas id="pick"></canvas>
+    <canvas id="outline"></canvas>
+  </body>
+</html>
+<script src="./shapefile.js"></script>
+<script src="./make-geo-picking-texture.js"></script>
+

+ 274 - 0
threejs/lessons/tools/geo-picking/make-geo-picking-texture.js

@@ -0,0 +1,274 @@
+'use strict';
+
+/* global shapefile */
+
+/* eslint no-console: off */
+/* eslint no-unused-vars: off */
+
+async function main() {
+  const size = 4096;
+  const pickCtx = document.querySelector('#pick').getContext('2d');
+  pickCtx.canvas.width = size;
+  pickCtx.canvas.height = size;
+
+  const outlineCtx = document.querySelector('#outline').getContext('2d');
+  outlineCtx.canvas.width = size;
+  outlineCtx.canvas.height = size;
+  outlineCtx.translate(outlineCtx.canvas.width / 2, outlineCtx.canvas.height / 2);
+  outlineCtx.scale(outlineCtx.canvas.width / 360, outlineCtx.canvas.height / -180);
+  outlineCtx.strokeStyle = '#FFF';
+
+  const workCtx = document.createElement('canvas').getContext('2d');
+  workCtx.canvas.width = size;
+  workCtx.canvas.height = size;
+
+  let id = 1;
+  const countryData = {};
+  const countriesById = [];
+  let min;
+  let max;
+
+  function resetMinMax() {
+    min = [ 10000,  10000];
+    max = [-10000, -10000];
+  }
+
+  function minMax(p) {
+    min[0] = Math.min(min[0], p[0]);
+    min[1] = Math.min(min[1], p[1]);
+    max[0] = Math.max(max[0], p[0]);
+    max[1] = Math.max(max[1], p[1]);
+  }
+
+  const geoHandlers = {
+    'MultiPolygon': multiPolygonArea,
+    'Polygon': polygonArea,
+  };
+
+  function multiPolygonArea(ctx, geo, drawFn) {
+    const {coordinates} = geo;
+    for (const polygon of coordinates) {
+      ctx.beginPath();
+      for (const ring of polygon) {
+        ring.forEach(minMax);
+        ctx.moveTo(...ring[0]);
+        for (let i = 0; i < ring.length; ++i) {
+          ctx.lineTo(...ring[i]);
+        }
+        ctx.closePath();
+      }
+      drawFn(ctx);
+    }
+  }
+
+  function polygonArea(ctx, geo, drawFn) {
+    const {coordinates} = geo;
+    ctx.beginPath();
+    for (const ring of coordinates) {
+      ring.forEach(minMax);
+      ctx.moveTo(...ring[0]);
+      for (let i = 0; i < ring.length; ++i) {
+        ctx.lineTo(...ring[i]);
+      }
+      ctx.closePath();
+    }
+    drawFn(ctx);
+  }
+
+  function fill(ctx) {
+    ctx.fill('evenodd');
+  }
+
+  function stroke(ctx) {
+    ctx.save();
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  function draw(area) {
+    const {properties, geometry} = area;
+    const {type} = geometry;
+    const name = properties.NAME;
+
+    console.log(name);
+
+    if (!countryData[name]) {
+      const r = (id >>  0) & 0xFF;
+      const g = (id >>  8) & 0xFF;
+      const b = (id >> 16) & 0xFF;
+
+      countryData[name] = {
+        color: [r, g, b],
+        id: id++,
+      };
+      countriesById.push({name});
+    }
+    const countryInfo = countriesById[countryData[name].id - 1];
+
+    const handler = geoHandlers[type];
+    if (!handler) {
+      throw new Error('unknown geometry type:', type);
+    }
+
+    resetMinMax();
+
+    workCtx.save();
+    workCtx.clearRect(0, 0, workCtx.canvas.width, workCtx.canvas.height);
+    workCtx.fillStyle = '#000';
+    workCtx.strokeStyle = '#000';
+    workCtx.translate(workCtx.canvas.width / 2, workCtx.canvas.height / 2);
+    workCtx.scale(workCtx.canvas.width / 360, workCtx.canvas.height / -180);
+
+    handler(workCtx, geometry, fill);
+
+    workCtx.restore();
+
+    countryInfo.min = min;
+    countryInfo.max = max;
+    countryInfo.area = properties.AREA;
+    countryInfo.lat = properties.LAT;
+    countryInfo.lon = properties.LON;
+    countryInfo.population = {
+      '2005': properties.POP2005,
+    };
+
+    //
+    const left   = Math.floor(( min[0] + 180) * workCtx.canvas.width  / 360);
+    const bottom = Math.floor((-min[1] +  90) * workCtx.canvas.height / 180);
+    const right  = Math.ceil( ( max[0] + 180) * workCtx.canvas.width  / 360);
+    const top    = Math.ceil( (-max[1] +  90) * workCtx.canvas.height / 180);
+    const width  = right - left + 1;
+    const height = Math.max(1, bottom - top + 1);
+
+    const color = countryData[name].color;
+    const src = workCtx.getImageData(left, top, width, height);
+    for (let y = 0; y < height; ++y) {
+      for (let x = 0; x < width; ++x) {
+        const off = (y * width + x) * 4;
+        if (src.data[off + 3]) {
+          src.data[off + 0] = color[0];
+          src.data[off + 1] = color[1];
+          src.data[off + 2] = color[2];
+          src.data[off + 3] = 255;
+        }
+      }
+    }
+    workCtx.putImageData(src, left, top);
+    pickCtx.drawImage(workCtx.canvas, 0, 0);
+
+//    handler(outlineCtx, geometry, stroke);
+  }
+
+  const source = await shapefile.open('TM_WORLD_BORDERS-0.3.shp');
+  const areas = [];
+  for (let i = 0; ; ++i) {
+    const {done, value} = await source.read();
+    if (done) {
+      break;
+    }
+    areas.push(value);
+    draw(value);
+    if (i % 20 === 19) {
+      await wait();
+    }
+  }
+  console.log(JSON.stringify(areas));
+
+  console.log('min', min);
+  console.log('max', max);
+
+  console.log(JSON.stringify(countriesById, null, 2));
+
+  const pick = pickCtx.getImageData(0, 0, pickCtx.canvas.width, pickCtx.canvas.height);
+  const outline = outlineCtx.getImageData(0, 0, outlineCtx.canvas.width, outlineCtx.canvas.height);
+
+  function getId(imageData, x, y) {
+    const off = (((y + imageData.height) % imageData.height) * imageData.width + ((x + imageData.width) % imageData.width)) * 4;
+    return imageData.data[off + 0] +
+           imageData.data[off + 1] * 256 +
+           imageData.data[off + 2] * 256 * 256 +
+           imageData.data[off + 3] * 256 * 256 * 256;
+  }
+
+  function putPixel(imageData, x, y, color) {
+    const off = (y * imageData.width + x) * 4;
+    imageData.data.set(color, off);
+  }
+
+
+  for (let y = 0; y < pick.height; ++y) {
+    for (let x = 0; x < pick.width; ++x) {
+      const s = getId(pick, x, y);
+      const r = getId(pick, x + 1, y);
+      const d = getId(pick, x, y + 1);
+      let v = 0;
+      if (s !== r || s !== d) {
+        v = 255;
+      }
+      putPixel(outline, x, y, [v, v, v, v]);
+    }
+  }
+
+  for (let y = 0; y < outline.height; ++y) {
+    for (let x = 0; x < outline.width; ++x) {
+      const s = getId(outline, x, y);
+      const l = getId(outline, x - 1, y);
+      const u = getId(outline, x, y - 1);
+      const r = getId(outline, x + 1, y);
+      const d = getId(outline, x, y + 1);
+      //const rd = getId(outline, x + 1, y + 1);
+      let v = s;
+      if ((s && r && d) ||
+          (s && l && d) ||
+          (s && r && u) ||
+          (s && l && u)) {
+        v = 0;
+      }
+      putPixel(outline, x, y, [v, v, v, v]);
+    }
+  }
+
+  outlineCtx.putImageData(outline, 0, 0);
+}
+
+function wait(ms = 0) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+function r(min, max) {
+  if (min === undefined) {
+    min = 0;
+    max = 1;
+  } else if (max === undefined){
+    max = min;
+    min = 0;
+  }
+  return min + Math.random() * (max - min);
+}
+
+function rgb(r, g, b) {
+  return `rgb(${r * 255 | 0},${g * 255 | 0},${b * 255 | 0})`;
+}
+
+function hsl(h, s, l) {
+  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+}
+
+function base64ToUint8Array(base64) {
+  const raw = window.atob(base64);
+  const rawLength = raw.length;
+  const array = new Uint8Array(new ArrayBuffer(rawLength));
+
+  for (let i = 0; i < rawLength; ++i) {
+    array[i] = raw.charCodeAt(i);
+  }
+
+  return array;
+}
+
+main();
+
+

+ 501 - 0
threejs/lessons/tools/geo-picking/shapefile.js

@@ -0,0 +1,501 @@
+// https://github.com/mbostock/shapefile Version 0.6.6. Copyright 2017 Mike Bostock.
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+	typeof define === 'function' && define.amd ? define(['exports'], factory) :
+	(factory((global.shapefile = {})));
+}(this, (function (exports) { 'use strict';
+
+var array_cancel = function() {
+  this._array = null;
+  return Promise.resolve();
+};
+
+var array_read = function() {
+  var array = this._array;
+  this._array = null;
+  return Promise.resolve(array ? {done: false, value: array} : {done: true, value: undefined});
+};
+
+function array(array) {
+  return new ArraySource(array instanceof Uint8Array ? array : new Uint8Array(array));
+}
+
+function ArraySource(array) {
+  this._array = array;
+}
+
+ArraySource.prototype.read = array_read;
+ArraySource.prototype.cancel = array_cancel;
+
+var fetchPath = function(url) {
+  return fetch(url).then(function(response) {
+    return response.body && response.body.getReader
+        ? response.body.getReader()
+        : response.arrayBuffer().then(array);
+  });
+};
+
+var requestPath = function(url) {
+  return new Promise(function(resolve, reject) {
+    var request = new XMLHttpRequest;
+    request.responseType = "arraybuffer";
+    request.onload = function() { resolve(array(request.response)); };
+    request.onerror = reject;
+    request.ontimeout = reject;
+    request.open("GET", url, true);
+    request.send();
+  });
+};
+
+function path(path) {
+  return (typeof fetch === "function" ? fetchPath : requestPath)(path);
+}
+
+function stream(source) {
+  return typeof source.read === "function" ? source : source.getReader();
+}
+
+var empty = new Uint8Array(0);
+
+var slice_cancel = function() {
+  return this._source.cancel();
+};
+
+function concat(a, b) {
+  if (!a.length) return b;
+  if (!b.length) return a;
+  var c = new Uint8Array(a.length + b.length);
+  c.set(a);
+  c.set(b, a.length);
+  return c;
+}
+
+var slice_read = function() {
+  var that = this, array = that._array.subarray(that._index);
+  return that._source.read().then(function(result) {
+    that._array = empty;
+    that._index = 0;
+    return result.done ? (array.length > 0
+        ? {done: false, value: array}
+        : {done: true, value: undefined})
+        : {done: false, value: concat(array, result.value)};
+  });
+};
+
+var slice_slice = function(length) {
+  if ((length |= 0) < 0) throw new Error("invalid length");
+  var that = this, index = this._array.length - this._index;
+
+  // If the request fits within the remaining buffer, resolve it immediately.
+  if (this._index + length <= this._array.length) {
+    return Promise.resolve(this._array.subarray(this._index, this._index += length));
+  }
+
+  // Otherwise, read chunks repeatedly until the request is fulfilled.
+  var array = new Uint8Array(length);
+  array.set(this._array.subarray(this._index));
+  return (function read() {
+    return that._source.read().then(function(result) {
+
+      // When done, it’s possible the request wasn’t fully fullfilled!
+      // If so, the pre-allocated array is too big and needs slicing.
+      if (result.done) {
+        that._array = empty;
+        that._index = 0;
+        return index > 0 ? array.subarray(0, index) : null;
+      }
+
+      // If this chunk fulfills the request, return the resulting array.
+      if (index + result.value.length >= length) {
+        that._array = result.value;
+        that._index = length - index;
+        array.set(result.value.subarray(0, length - index), index);
+        return array;
+      }
+
+      // Otherwise copy this chunk into the array, then read the next chunk.
+      array.set(result.value, index);
+      index += result.value.length;
+      return read();
+    });
+  })();
+};
+
+function slice(source) {
+  return typeof source.slice === "function" ? source :
+      new SliceSource(typeof source.read === "function" ? source
+          : source.getReader());
+}
+
+function SliceSource(source) {
+  this._source = source;
+  this._array = empty;
+  this._index = 0;
+}
+
+SliceSource.prototype.read = slice_read;
+SliceSource.prototype.slice = slice_slice;
+SliceSource.prototype.cancel = slice_cancel;
+
+var dbf_cancel = function() {
+  return this._source.cancel();
+};
+
+var readBoolean = function(value) {
+  return /^[nf]$/i.test(value) ? false
+      : /^[yt]$/i.test(value) ? true
+      : null;
+};
+
+var readDate = function(value) {
+  return new Date(+value.substring(0, 4), value.substring(4, 6) - 1, +value.substring(6, 8));
+};
+
+var readNumber = function(value) {
+  return !(value = value.trim()) || isNaN(value = +value) ? null : value;
+};
+
+var readString = function(value) {
+  return value.trim() || null;
+};
+
+var types = {
+  B: readNumber,
+  C: readString,
+  D: readDate,
+  F: readNumber,
+  L: readBoolean,
+  M: readNumber,
+  N: readNumber
+};
+
+var dbf_read = function() {
+  var that = this, i = 1;
+  return that._source.slice(that._recordLength).then(function(value) {
+    return value && (value[0] !== 0x1a) ? {done: false, value: that._fields.reduce(function(p, f) {
+      p[f.name] = types[f.type](that._decode(value.subarray(i, i += f.length)));
+      return p;
+    }, {})} : {done: true, value: undefined};
+  });
+};
+
+var view = function(array) {
+  return new DataView(array.buffer, array.byteOffset, array.byteLength);
+};
+
+var dbf = function(source, decoder) {
+  source = slice(source);
+  return source.slice(32).then(function(array) {
+    var head = view(array);
+    return source.slice(head.getUint16(8, true) - 32).then(function(array) {
+      return new Dbf(source, decoder, head, view(array));
+    });
+  });
+};
+
+function Dbf(source, decoder, head, body) {
+  this._source = source;
+  this._decode = decoder.decode.bind(decoder);
+  this._recordLength = head.getUint16(10, true);
+  this._fields = [];
+  for (var n = 0; body.getUint8(n) !== 0x0d; n += 32) {
+    for (var j = 0; j < 11; ++j) if (body.getUint8(n + j) === 0) break;
+    this._fields.push({
+      name: this._decode(new Uint8Array(body.buffer, body.byteOffset + n, j)),
+      type: String.fromCharCode(body.getUint8(n + 11)),
+      length: body.getUint8(n + 16)
+    });
+  }
+}
+
+var prototype = Dbf.prototype;
+prototype.read = dbf_read;
+prototype.cancel = dbf_cancel;
+
+function cancel() {
+  return this._source.cancel();
+}
+
+var parseMultiPoint = function(record) {
+  var i = 40, j, n = record.getInt32(36, true), coordinates = new Array(n);
+  for (j = 0; j < n; ++j, i += 16) coordinates[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)];
+  return {type: "MultiPoint", coordinates: coordinates};
+};
+
+var parseNull = function() {
+  return null;
+};
+
+var parsePoint = function(record) {
+  return {type: "Point", coordinates: [record.getFloat64(4, true), record.getFloat64(12, true)]};
+};
+
+var parsePolygon = function(record) {
+  var i = 44, j, n = record.getInt32(36, true), m = record.getInt32(40, true), parts = new Array(n), points = new Array(m), polygons = [], holes = [];
+  for (j = 0; j < n; ++j, i += 4) parts[j] = record.getInt32(i, true);
+  for (j = 0; j < m; ++j, i += 16) points[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)];
+
+  parts.forEach(function(i, j) {
+    var ring = points.slice(i, parts[j + 1]);
+    if (ringClockwise(ring)) polygons.push([ring]);
+    else holes.push(ring);
+  });
+
+  holes.forEach(function(hole) {
+    polygons.some(function(polygon) {
+      if (ringContainsSome(polygon[0], hole)) {
+        polygon.push(hole);
+        return true;
+      }
+    }) || polygons.push([hole]);
+  });
+
+  return polygons.length === 1
+      ? {type: "Polygon", coordinates: polygons[0]}
+      : {type: "MultiPolygon", coordinates: polygons};
+};
+
+function ringClockwise(ring) {
+  if ((n = ring.length) < 4) return false;
+  var i = 0, n, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1];
+  while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1];
+  return area >= 0;
+}
+
+function ringContainsSome(ring, hole) {
+  var i = -1, n = hole.length, c;
+  while (++i < n) {
+    if (c = ringContains(ring, hole[i])) {
+      return c > 0;
+    }
+  }
+  return false;
+}
+
+function ringContains(ring, point) {
+  var x = point[0], y = point[1], contains = -1;
+  for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) {
+    var pi = ring[i], xi = pi[0], yi = pi[1],
+        pj = ring[j], xj = pj[0], yj = pj[1];
+    if (segmentContains(pi, pj, point)) {
+      return 0;
+    }
+    if (((yi > y) !== (yj > y)) && ((x < (xj - xi) * (y - yi) / (yj - yi) + xi))) {
+      contains = -contains;
+    }
+  }
+  return contains;
+}
+
+function segmentContains(p0, p1, p2) {
+  var x20 = p2[0] - p0[0], y20 = p2[1] - p0[1];
+  if (x20 === 0 && y20 === 0) return true;
+  var x10 = p1[0] - p0[0], y10 = p1[1] - p0[1];
+  if (x10 === 0 && y10 === 0) return false;
+  var t = (x20 * x10 + y20 * y10) / (x10 * x10 + y10 * y10);
+  return t < 0 || t > 1 ? false : t === 0 || t === 1 ? true : t * x10 === x20 && t * y10 === y20;
+}
+
+var parsePolyLine = function(record) {
+  var i = 44, j, n = record.getInt32(36, true), m = record.getInt32(40, true), parts = new Array(n), points = new Array(m);
+  for (j = 0; j < n; ++j, i += 4) parts[j] = record.getInt32(i, true);
+  for (j = 0; j < m; ++j, i += 16) points[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)];
+  return n === 1
+      ? {type: "LineString", coordinates: points}
+      : {type: "MultiLineString", coordinates: parts.map(function(i, j) { return points.slice(i, parts[j + 1]); })};
+};
+
+var concat$1 = function(a, b) {
+  var ab = new Uint8Array(a.length + b.length);
+  ab.set(a, 0);
+  ab.set(b, a.length);
+  return ab;
+};
+
+var shp_read = function() {
+  var that = this;
+  ++that._index;
+  return that._source.slice(12).then(function(array) {
+    if (array == null) return {done: true, value: undefined};
+    var header = view(array);
+
+    // If the record starts with an invalid shape type (see #36), scan ahead in
+    // four-byte increments to find the next valid record, identified by the
+    // expected index, a non-empty content length and a valid shape type.
+    function skip() {
+      return that._source.slice(4).then(function(chunk) {
+        if (chunk == null) return {done: true, value: undefined};
+        header = view(array = concat$1(array.slice(4), chunk));
+        return header.getInt32(0, false) !== that._index ? skip() : read();
+      });
+    }
+
+    // All records should have at least four bytes (for the record shape type),
+    // so an invalid content length indicates corruption.
+    function read() {
+      var length = header.getInt32(4, false) * 2 - 4, type = header.getInt32(8, true);
+      return length < 0 || (type && type !== that._type) ? skip() : that._source.slice(length).then(function(chunk) {
+        return {done: false, value: type ? that._parse(view(concat$1(array.slice(8), chunk))) : null};
+      });
+    }
+
+    return read();
+  });
+};
+
+var parsers = {
+  0: parseNull,
+  1: parsePoint,
+  3: parsePolyLine,
+  5: parsePolygon,
+  8: parseMultiPoint,
+  11: parsePoint, // PointZ
+  13: parsePolyLine, // PolyLineZ
+  15: parsePolygon, // PolygonZ
+  18: parseMultiPoint, // MultiPointZ
+  21: parsePoint, // PointM
+  23: parsePolyLine, // PolyLineM
+  25: parsePolygon, // PolygonM
+  28: parseMultiPoint // MultiPointM
+};
+
+var shp = function(source) {
+  source = slice(source);
+  return source.slice(100).then(function(array) {
+    return new Shp(source, view(array));
+  });
+};
+
+function Shp(source, header) {
+  var type = header.getInt32(32, true);
+  if (!(type in parsers)) throw new Error("unsupported shape type: " + type);
+  this._source = source;
+  this._type = type;
+  this._index = 0;
+  this._parse = parsers[type];
+  this.bbox = [header.getFloat64(36, true), header.getFloat64(44, true), header.getFloat64(52, true), header.getFloat64(60, true)];
+}
+
+var prototype$2 = Shp.prototype;
+prototype$2.read = shp_read;
+prototype$2.cancel = cancel;
+
+function noop() {}
+
+var shapefile_cancel = function() {
+  return Promise.all([
+    this._dbf && this._dbf.cancel(),
+    this._shp.cancel()
+  ]).then(noop);
+};
+
+var shapefile_read = function() {
+  var that = this;
+  return Promise.all([
+    that._dbf ? that._dbf.read() : {value: {}},
+    that._shp.read()
+  ]).then(function(results) {
+    var dbf = results[0], shp = results[1];
+    return shp.done ? shp : {
+      done: false,
+      value: {
+        type: "Feature",
+        properties: dbf.value,
+        geometry: shp.value
+      }
+    };
+  });
+};
+
+var shapefile = function(shpSource, dbfSource, decoder) {
+  return Promise.all([
+    shp(shpSource),
+    dbfSource && dbf(dbfSource, decoder)
+  ]).then(function(sources) {
+    return new Shapefile(sources[0], sources[1]);
+  });
+};
+
+function Shapefile(shp$$1, dbf$$1) {
+  this._shp = shp$$1;
+  this._dbf = dbf$$1;
+  this.bbox = shp$$1.bbox;
+}
+
+var prototype$1 = Shapefile.prototype;
+prototype$1.read = shapefile_read;
+prototype$1.cancel = shapefile_cancel;
+
+function open(shp$$1, dbf$$1, options) {
+  if (typeof dbf$$1 === "string") {
+    if (!/\.dbf$/.test(dbf$$1)) dbf$$1 += ".dbf";
+    dbf$$1 = path(dbf$$1, options);
+  } else if (dbf$$1 instanceof ArrayBuffer || dbf$$1 instanceof Uint8Array) {
+    dbf$$1 = array(dbf$$1);
+  } else if (dbf$$1 != null) {
+    dbf$$1 = stream(dbf$$1);
+  }
+  if (typeof shp$$1 === "string") {
+    if (!/\.shp$/.test(shp$$1)) shp$$1 += ".shp";
+    if (dbf$$1 === undefined) dbf$$1 = path(shp$$1.substring(0, shp$$1.length - 4) + ".dbf", options).catch(function() {});
+    shp$$1 = path(shp$$1, options);
+  } else if (shp$$1 instanceof ArrayBuffer || shp$$1 instanceof Uint8Array) {
+    shp$$1 = array(shp$$1);
+  } else {
+    shp$$1 = stream(shp$$1);
+  }
+  return Promise.all([shp$$1, dbf$$1]).then(function(sources) {
+    var shp$$1 = sources[0], dbf$$1 = sources[1], encoding = "windows-1252";
+    if (options && options.encoding != null) encoding = options.encoding;
+    return shapefile(shp$$1, dbf$$1, dbf$$1 && new TextDecoder(encoding));
+  });
+}
+
+function openShp(source, options) {
+  if (typeof source === "string") {
+    if (!/\.shp$/.test(source)) source += ".shp";
+    source = path(source, options);
+  } else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
+    source = array(source);
+  } else {
+    source = stream(source);
+  }
+  return Promise.resolve(source).then(shp);
+}
+
+function openDbf(source, options) {
+  var encoding = "windows-1252";
+  if (options && options.encoding != null) encoding = options.encoding;
+  encoding = new TextDecoder(encoding);
+  if (typeof source === "string") {
+    if (!/\.dbf$/.test(source)) source += ".dbf";
+    source = path(source, options);
+  } else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
+    source = array(source);
+  } else {
+    source = stream(source);
+  }
+  return Promise.resolve(source).then(function(source) {
+    return dbf(source, encoding);
+  });
+}
+
+function read(shp$$1, dbf$$1, options) {
+  return open(shp$$1, dbf$$1, options).then(function(source) {
+    var features = [], collection = {type: "FeatureCollection", features: features, bbox: source.bbox};
+    return source.read().then(function read(result) {
+      if (result.done) return collection;
+      features.push(result.value);
+      return source.read().then(read);
+    });
+  });
+}
+
+exports.open = open;
+exports.openShp = openShp;
+exports.openDbf = openDbf;
+exports.read = read;
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+})));

+ 231 - 0
threejs/lessons/tools/ogc-parser.js

@@ -0,0 +1,231 @@
+'use strict';
+
+const assert = {
+  strictEqual(actual, expected, ...args) {
+    args = args || [];
+    if (actual !== expected) {
+      throw new Error(`${actual} (actual) should equal ${expected} (expected): ${[...args].join(' ')}`);
+    }
+  },
+  notStrictEqual(actual, expected, ...args) {
+    args = args || [];
+    if (actual === expected) {
+      throw new Error(`${actual} (actual) should NOT equal ${expected} (expected): ${[...args].join(' ')}`);
+    }
+  },
+}
+
+function dumpBuf(buf) {
+  for (let i = 0; i < buf.length; i += 32) {
+    const p = [];
+    const a = [];
+    for (let j = i; j < i + 32 && j < buf.length; ++j) {
+      const b = buf[j];
+      p.push(b.toString(16).padStart(2, '0'));
+      a.push(b >= 32 && b < 128 ? String.fromCharCode(b) : '.');
+      if (j % 4 === 3) {
+        p.push(' ');
+      }
+    }
+    console.log(i.toString(16).padStart(8, '0'), ':', p.join(''), a.join(''));
+  }
+}
+
+function parse(buf) {
+  assert.strictEqual(buf[0], 0x47, 'bad header');
+  assert.strictEqual(buf[1], 0x50, 'bad header');
+  assert.strictEqual(buf[2], 0, 'unknown version');  // version
+  const flags = buf[3];
+
+  const flag_x         = (flags >> 5) & 1;
+  const flag_empty_geo = (flags >> 4) & 1;  // 1 = empty, 0 non-empty
+  const flag_byteOrder = (flags >> 0) & 1;  // 1 = little endian, 0 = big
+  const flag_envelope  = (flags >> 1) & 7;
+
+  assert.strictEqual(flag_x, 0, 'x must be 0');
+
+  const envelopeSizes = [
+    0,  // 0: non
+    4,  // 1: minx, maxx, miny, maxy
+    6,  // 2: minx, maxx, miny, maxy, minz, maxz
+    6,  // 3: minx, maxx, miny, maxy, minm, maxm
+    8,  // 4: minx, maxx, miny, maxy, minz, maxz, minm, maxm
+  ];
+
+  const envelopeSize = envelopeSizes[flag_envelope];
+  assert.notStrictEqual(envelopeSize, undefined);
+
+  const headerSize = 8;
+  let cursor = headerSize;
+
+  const dataView = new DataView(buf.buffer);
+  /*
+  const readBE = {
+    getDouble() { const v = buf.readDoubleBE(cursor); cursor += 8 ; return v; },
+    getFloat()  { const v = buf.readFloatBE(cursor);  cursor += 4 ; return v; },
+    getInt8()   { const v = buf.readInt8(cursor);     cursor += 1 ; return v; },
+    getUint8()  { const v = buf.readUInt8(cursor);    cursor += 1 ; return v; },
+    getInt16()  { const v = buf.readInt16BE(cursor);  cursor += 2 ; return v; },
+    getUint16() { const v = buf.readUInt16BE(cursor); cursor += 2 ; return v; },
+    getInt32()  { const v = buf.readInt32BE(cursor);  cursor += 4 ; return v; },
+    getUint32() { const v = buf.readUInt32BE(cursor); cursor += 4 ; return v; },
+  };
+
+  const readLE = {
+    getDouble() { const v = buf.readDoubleLE(cursor); cursor += 8 ; return v; },
+    getFloat()  { const v = buf.readFloatLE(cursor);  cursor += 4 ; return v; },
+    getInt8()   { const v = buf.readInt8(cursor);     cursor += 1 ; return v; },
+    getUint8()  { const v = buf.readUInt8(cursor);    cursor += 1 ; return v; },
+    getInt16()  { const v = buf.readInt16LE(cursor);  cursor += 2 ; return v; },
+    getUint16() { const v = buf.readUInt16LE(cursor); cursor += 2 ; return v; },
+    getInt32()  { const v = buf.readInt32LE(cursor);  cursor += 4 ; return v; },
+    getUint32() { const v = buf.readUInt32LE(cursor); cursor += 4 ; return v; },
+  };
+  */
+
+  let littleEndian;
+  let endianStack = [];
+
+  function pushByteOrder(byteOrder) {
+    endianStack.push(littleEndian);
+    littleEndian = byteOrder;
+  }
+
+  function popByteOrder() {
+    littleEndian = endianStack.pop();
+  }
+
+  const getDouble = () => { const v = dataView.getFloat64(cursor, littleEndian); cursor += 8 ; return v; };
+  const getFloat =  () => { const v = dataView.getFloat32(cursor, littleEndian); cursor += 4 ; return v; };
+  const getInt8 =   () => { const v = dataView.getInt8(cursor, littleEndian);    cursor += 1 ; return v; };
+  const getUint8 =  () => { const v = dataView.getUint8(cursor, littleEndian);   cursor += 1 ; return v; };
+  const getInt16 =  () => { const v = dataView.getInt16(cursor, littleEndian);   cursor += 2 ; return v; };
+  const getUint16 = () => { const v = dataView.getUint16(cursor, littleEndian);  cursor += 2 ; return v; };
+  const getInt32 =  () => { const v = dataView.getInt32(cursor, littleEndian);   cursor += 4 ; return v; };
+  const getUint32 = () => { const v = dataView.getUint32(cursor, littleEndian);  cursor += 4 ; return v; };
+
+  pushByteOrder(flag_byteOrder);
+
+  const envelope = [];
+  for (let i = 0; i < envelopeSize; ++i) {
+    envelope.push(getDouble());
+  }
+
+  const primitives = [];
+
+  function getPoints(num) {
+    const points = [];
+    for (let i = 0; i < num; ++i) {
+      points.push(getDouble(), getDouble());
+    }
+    return points;
+  }
+
+  function getRings(num) {
+    const rings = [];
+    for (let i = 0; i < num; ++i) {
+      rings.push(getPoints(getUint32()));
+    }
+    return rings;
+  }
+
+  function pointHandler() {
+    return {
+      type: 'point',
+      point: getPoints(1),
+    };
+  }
+
+  function lineStringHandler() {
+    return {
+      type: 'lineString',
+      points: getPoints(getUint32()),
+    };
+  }
+
+  function polygonHandler() {
+    return {
+      type: 'polygon',
+      rings: getRings(getUint32()),
+    };
+  }
+
+  function multiPointHandler() {
+    // WTF?
+    const points = [];
+    const num = getUint32();
+    for (let i = 0; i < num; ++i) {
+      pushByteOrder(getInt8());
+      const type = getUint32();
+      assert.strictEqual(type, 1);  // must be point
+      points.push(getDouble(), getDouble());
+      popByteOrder();
+    }
+    return {
+      type: 'multiPoint',
+      points,
+    };
+  }
+
+  function multiLineStringHandler() {
+    // WTF?
+    const lineStrings = [];
+    const num = getUint32();
+    for (let i = 0; i < num; ++i) {
+      pushByteOrder(getInt8());
+      const type = getUint32();
+      assert.strictEqual(type, 2);  // must be lineString
+      lineStrings.push(getPoints(getUint32()));
+      popByteOrder();
+    }
+    return {
+      type: 'multiLineString',
+      lineStrings,
+    };
+  }
+
+  function multiPolygonHandler() {
+    // WTF?
+    const polygons = [];
+    const num = getUint32();
+    for (let i = 0; i < num; ++i) {
+      pushByteOrder(getInt8());
+      const type = getUint32();
+      assert.strictEqual(type, 3);  // must be polygon
+      polygons.push(getRings(getUint32()));
+      popByteOrder();
+    }
+    return {
+      type: 'multiPolygon',
+      polygons,
+    };
+  }
+
+  const typeHandlers = [
+    undefined,              // 0
+    pointHandler,           // 1
+    lineStringHandler,      // 2
+    polygonHandler,         // 3
+    multiPointHandler,      // 4
+    multiLineStringHandler, // 5,
+    multiPolygonHandler,    // 6,
+  ];
+
+  const end = buf.length;
+  while (cursor < end) {
+    pushByteOrder(getInt8());
+    const type = getUint32();
+    const handler = typeHandlers[type];
+    assert.notStrictEqual(handler, undefined, 'unknown type');
+    primitives.push(handler());
+    popByteOrder();
+  }
+
+  return {
+    envelope,
+    primitives,
+  };
+}
+
+window.ogcParser = {parse};
+

+ 4184 - 0
threejs/resources/data/world/country-info.json

@@ -0,0 +1,4184 @@
+[
+  {
+    "name": "Antigua and Barbuda",
+    "min": [
+      -61.89111299999996,
+      16.98971899999998
+    ],
+    "max": [
+      -61.66638899999998,
+      17.724998000000028
+    ],
+    "area": 44,
+    "lat": 17.078,
+    "lon": -61.783,
+    "population": {
+      "2005": 83039
+    }
+  },
+  {
+    "name": "Algeria",
+    "min": [
+      -8.667223,
+      18.976387
+    ],
+    "max": [
+      11.986475,
+      37.091385
+    ],
+    "area": 238174,
+    "lat": 28.163,
+    "lon": 2.632,
+    "population": {
+      "2005": 32854159
+    }
+  },
+  {
+    "name": "Azerbaijan",
+    "min": [
+      44.778862000000004,
+      38.38915300000008
+    ],
+    "max": [
+      50.37499199999996,
+      41.89705700000002
+    ],
+    "area": 8260,
+    "lat": 40.43,
+    "lon": 47.395,
+    "population": {
+      "2005": 8352021
+    }
+  },
+  {
+    "name": "Albania",
+    "min": [
+      19.282497,
+      39.644722
+    ],
+    "max": [
+      21.054165,
+      42.661942
+    ],
+    "area": 2740,
+    "lat": 41.143,
+    "lon": 20.068,
+    "population": {
+      "2005": 3153731
+    }
+  },
+  {
+    "name": "Armenia",
+    "min": [
+      43.45388800000006,
+      38.841147999999976
+    ],
+    "max": [
+      46.62248999999997,
+      41.29705000000013
+    ],
+    "area": 2820,
+    "lat": 40.534,
+    "lon": 44.563,
+    "population": {
+      "2005": 3017661
+    }
+  },
+  {
+    "name": "Angola",
+    "min": [
+      11.663332000000082,
+      -18.016391999999996
+    ],
+    "max": [
+      24.084442000000138,
+      -4.388990999999976
+    ],
+    "area": 124670,
+    "lat": -12.296,
+    "lon": 17.544,
+    "population": {
+      "2005": 16095214
+    }
+  },
+  {
+    "name": "American Samoa",
+    "min": [
+      -170.82611099999994,
+      -14.375554999999963
+    ],
+    "max": [
+      -169.43832399999994,
+      -14.166388999999924
+    ],
+    "area": 20,
+    "lat": -14.318,
+    "lon": -170.73,
+    "population": {
+      "2005": 64051
+    }
+  },
+  {
+    "name": "Argentina",
+    "min": [
+      -73.58361799999994,
+      -55.05167399999999
+    ],
+    "max": [
+      -53.649726999999984,
+      -21.780520999999965
+    ],
+    "area": 273669,
+    "lat": -35.377,
+    "lon": -65.167,
+    "population": {
+      "2005": 38747148
+    }
+  },
+  {
+    "name": "Australia",
+    "min": [
+      112.90721100000007,
+      -54.75389100000001
+    ],
+    "max": [
+      159.101898,
+      -10.051390000000026
+    ],
+    "area": 768230,
+    "lat": -24.973,
+    "lon": 136.189,
+    "population": {
+      "2005": 20310208
+    }
+  },
+  {
+    "name": "Bahrain",
+    "min": [
+      50.453049000000135,
+      25.571941000000038
+    ],
+    "max": [
+      50.822495,
+      26.288887000000045
+    ],
+    "area": 71,
+    "lat": 26.019,
+    "lon": 50.562,
+    "population": {
+      "2005": 724788
+    }
+  },
+  {
+    "name": "Barbados",
+    "min": [
+      -59.659447,
+      13.050554
+    ],
+    "max": [
+      -59.426949,
+      13.337221
+    ],
+    "area": 43,
+    "lat": 13.153,
+    "lon": -59.559,
+    "population": {
+      "2005": 291933
+    }
+  },
+  {
+    "name": "Bermuda",
+    "min": [
+      -64.87611400000003,
+      32.26055100000002
+    ],
+    "max": [
+      -64.63862599999999,
+      32.38221700000008
+    ],
+    "area": 5,
+    "lat": 32.336,
+    "lon": -64.709,
+    "population": {
+      "2005": 64174
+    }
+  },
+  {
+    "name": "Bahamas",
+    "min": [
+      -78.97889699999996,
+      20.91527600000012
+    ],
+    "max": [
+      -72.737503,
+      26.92916500000007
+    ],
+    "area": 1001,
+    "lat": 24.628,
+    "lon": -78.014,
+    "population": {
+      "2005": 323295
+    }
+  },
+  {
+    "name": "Bangladesh",
+    "min": [
+      88.04332,
+      20.738048999999933
+    ],
+    "max": [
+      92.66934200000003,
+      26.631939000000045
+    ],
+    "area": 13017,
+    "lat": 24.218,
+    "lon": 89.941,
+    "population": {
+      "2005": 15328112
+    }
+  },
+  {
+    "name": "Belize",
+    "min": [
+      -89.21640000000002,
+      15.889851000000135
+    ],
+    "max": [
+      -87.77889999999996,
+      18.489902000000086
+    ],
+    "area": 2281,
+    "lat": 17.219,
+    "lon": -88.602,
+    "population": {
+      "2005": 275546
+    }
+  },
+  {
+    "name": "Bosnia and Herzegovina",
+    "min": [
+      15.736387,
+      42.565826
+    ],
+    "max": [
+      19.621765,
+      45.265945
+    ],
+    "area": 5120,
+    "lat": 44.169,
+    "lon": 17.786,
+    "population": {
+      "2005": 3915238
+    }
+  },
+  {
+    "name": "Bolivia",
+    "min": [
+      -69.656189,
+      -22.901112
+    ],
+    "max": [
+      -57.521118,
+      -9.679195
+    ],
+    "area": 108438,
+    "lat": -16.715,
+    "lon": -64.671,
+    "population": {
+      "2005": 9182015
+    }
+  },
+  {
+    "name": "Burma",
+    "min": [
+      92.20498700000013,
+      9.786385999999936
+    ],
+    "max": [
+      101.17082200000004,
+      28.549164000000133
+    ],
+    "area": 65755,
+    "lat": 21.718,
+    "lon": 96.041,
+    "population": {
+      "2005": 47967266
+    }
+  },
+  {
+    "name": "Benin",
+    "min": [
+      0.776667,
+      6.218721
+    ],
+    "max": [
+      3.855,
+      12.396658
+    ],
+    "area": 11062,
+    "lat": 10.541,
+    "lon": 2.469,
+    "population": {
+      "2005": 8490301
+    }
+  },
+  {
+    "name": "Solomon Islands",
+    "min": [
+      155.507477,
+      -11.845833000000027
+    ],
+    "max": [
+      167.20996100000013,
+      -5.293055999999979
+    ],
+    "area": 2799,
+    "lat": -9.611,
+    "lon": 160.109,
+    "population": {
+      "2005": 472419
+    }
+  },
+  {
+    "name": "Brazil",
+    "min": [
+      -74.01055899999994,
+      -33.74389600000001
+    ],
+    "max": [
+      -29.839999999999975,
+      5.273888999999997
+    ],
+    "area": 845942,
+    "lat": -10.772,
+    "lon": -53.089,
+    "population": {
+      "2005": 186830759
+    }
+  },
+  {
+    "name": "Bulgaria",
+    "min": [
+      22.365276,
+      41.24305
+    ],
+    "max": [
+      28.606384,
+      44.224716
+    ],
+    "area": 11063,
+    "lat": 42.761,
+    "lon": 25.231,
+    "population": {
+      "2005": 7744591
+    }
+  },
+  {
+    "name": "Brunei Darussalam",
+    "min": [
+      114.09507799999994,
+      4.017498999999987
+    ],
+    "max": [
+      115.36026000000004,
+      5.053053999999975
+    ],
+    "area": 527,
+    "lat": 4.468,
+    "lon": 114.591,
+    "population": {
+      "2005": 373831
+    }
+  },
+  {
+    "name": "Canada",
+    "min": [
+      -141.002991,
+      41.67555199999998
+    ],
+    "max": [
+      -52.61444899999998,
+      83.11387600000012
+    ],
+    "area": 909351,
+    "lat": 59.081,
+    "lon": -109.433,
+    "population": {
+      "2005": 32270507
+    }
+  },
+  {
+    "name": "Cambodia",
+    "min": [
+      102.34554300000013,
+      10.422738999999922
+    ],
+    "max": [
+      107.63638300000008,
+      14.708618000000001
+    ],
+    "area": 17652,
+    "lat": 12.714,
+    "lon": 104.564,
+    "population": {
+      "2005": 13955507
+    }
+  },
+  {
+    "name": "Sri Lanka",
+    "min": [
+      79.6519320000001,
+      5.917777000000058
+    ],
+    "max": [
+      81.89166299999994,
+      9.828331000000048
+    ],
+    "area": 6463,
+    "lat": 7.612,
+    "lon": 80.704,
+    "population": {
+      "2005": 19120763
+    }
+  },
+  {
+    "name": "Congo",
+    "min": [
+      11.140661,
+      -5.019444
+    ],
+    "max": [
+      18.643608,
+      3.713055
+    ],
+    "area": 34150,
+    "lat": -0.055,
+    "lon": 15.986,
+    "population": {
+      "2005": 3609851
+    }
+  },
+  {
+    "name": "Democratic Republic of the Congo",
+    "min": [
+      12.21455200000014,
+      -13.45805699999994
+    ],
+    "max": [
+      31.302775999999994,
+      5.381389000000013
+    ],
+    "area": 226705,
+    "lat": -2.876,
+    "lon": 23.654,
+    "population": {
+      "2005": 58740547
+    }
+  },
+  {
+    "name": "Burundi",
+    "min": [
+      28.983887,
+      -4.448056
+    ],
+    "max": [
+      30.853886,
+      -2.298056
+    ],
+    "area": 2568,
+    "lat": -3.356,
+    "lon": 29.887,
+    "population": {
+      "2005": 7858791
+    }
+  },
+  {
+    "name": "China",
+    "min": [
+      73.61720299999996,
+      18.168884000000048
+    ],
+    "max": [
+      134.77359000000013,
+      53.55443600000007
+    ],
+    "area": 932743,
+    "lat": 33.42,
+    "lon": 106.514,
+    "population": {
+      "2005": 1312978855
+    }
+  },
+  {
+    "name": "Afghanistan",
+    "min": [
+      60.504166,
+      29.406105
+    ],
+    "max": [
+      74.915741,
+      38.472115
+    ],
+    "area": 65209,
+    "lat": 33.677,
+    "lon": 65.216,
+    "population": {
+      "2005": 25067407
+    }
+  },
+  {
+    "name": "Bhutan",
+    "min": [
+      88.751938,
+      26.703049
+    ],
+    "max": [
+      92.115265,
+      28.325275
+    ],
+    "area": 4700,
+    "lat": 27.415,
+    "lon": 90.429,
+    "population": {
+      "2005": 637013
+    }
+  },
+  {
+    "name": "Chile",
+    "min": [
+      -109.44917299999997,
+      -55.91972399999992
+    ],
+    "max": [
+      -66.41917399999994,
+      -17.505279999999914
+    ],
+    "area": 74880,
+    "lat": -23.389,
+    "lon": -69.433,
+    "population": {
+      "2005": 16295102
+    }
+  },
+  {
+    "name": "Cayman Islands",
+    "min": [
+      -81.40112299999998,
+      19.264720999999952
+    ],
+    "max": [
+      -79.73278799999997,
+      19.762218000000075
+    ],
+    "area": 26,
+    "lat": 19.314,
+    "lon": -81.198,
+    "population": {
+      "2005": 45591
+    }
+  },
+  {
+    "name": "Cameroon",
+    "min": [
+      8.502222000000017,
+      1.6541660000000888
+    ],
+    "max": [
+      16.207222,
+      13.085278000000073
+    ],
+    "area": 46540,
+    "lat": 5.133,
+    "lon": 12.277,
+    "population": {
+      "2005": 17795149
+    }
+  },
+  {
+    "name": "Chad",
+    "min": [
+      13.461666,
+      7.457777
+    ],
+    "max": [
+      24.002747,
+      23.450554
+    ],
+    "area": 125920,
+    "lat": 15.361,
+    "lon": 18.665,
+    "population": {
+      "2005": 10145609
+    }
+  },
+  {
+    "name": "Comoros",
+    "min": [
+      43.21360800000002,
+      -12.383057000000008
+    ],
+    "max": [
+      44.53082999999998,
+      -11.36694499999993
+    ],
+    "area": 223,
+    "lat": -11.758,
+    "lon": 43.337,
+    "population": {
+      "2005": 797902
+    }
+  },
+  {
+    "name": "Colombia",
+    "min": [
+      -81.72277799999995,
+      -4.236873999999943
+    ],
+    "max": [
+      -66.87188700000002,
+      13.378611000000035
+    ],
+    "area": 103870,
+    "lat": 3.9,
+    "lon": -73.076,
+    "population": {
+      "2005": 4494579
+    }
+  },
+  {
+    "name": "Costa Rica",
+    "min": [
+      -85.91139199999992,
+      8.025668999999994
+    ],
+    "max": [
+      -82.56140099999993,
+      11.213610000000017
+    ],
+    "area": 5106,
+    "lat": 9.971,
+    "lon": -83.946,
+    "population": {
+      "2005": 4327228
+    }
+  },
+  {
+    "name": "Central African Republic",
+    "min": [
+      14.41861,
+      2.220833
+    ],
+    "max": [
+      27.460278,
+      11.001389
+    ],
+    "area": 62298,
+    "lat": 6.571,
+    "lon": 20.483,
+    "population": {
+      "2005": 4191429
+    }
+  },
+  {
+    "name": "Cuba",
+    "min": [
+      -84.95333900000003,
+      19.821940999999924
+    ],
+    "max": [
+      -74.13084399999997,
+      23.20416599999993
+    ],
+    "area": 10982,
+    "lat": 21.297,
+    "lon": -77.781,
+    "population": {
+      "2005": 11259905
+    }
+  },
+  {
+    "name": "Cape Verde",
+    "min": [
+      -25.360558000000026,
+      14.811110000000042
+    ],
+    "max": [
+      -22.665836000000013,
+      17.193054000000075
+    ],
+    "area": 403,
+    "lat": 15.071,
+    "lon": -23.634,
+    "population": {
+      "2005": 506807
+    }
+  },
+  {
+    "name": "Cook Islands",
+    "min": [
+      -165.85028099999997,
+      -21.940833999999995
+    ],
+    "max": [
+      -157.305878,
+      -8.948057000000006
+    ],
+    "area": 24,
+    "lat": -21.219,
+    "lon": -159.782,
+    "population": {
+      "2005": 13984
+    }
+  },
+  {
+    "name": "Cyprus",
+    "min": [
+      32.269165,
+      34.56255
+    ],
+    "max": [
+      34.590553,
+      35.690277
+    ],
+    "area": 924,
+    "lat": 35.043,
+    "lon": 33.219,
+    "population": {
+      "2005": 836321
+    }
+  },
+  {
+    "name": "Denmark",
+    "min": [
+      8.087221,
+      54.56166100000007
+    ],
+    "max": [
+      15.149999999999977,
+      57.746666000000005
+    ],
+    "area": 4243,
+    "lat": 56.058,
+    "lon": 9.264,
+    "population": {
+      "2005": 5416945
+    }
+  },
+  {
+    "name": "Djibouti",
+    "min": [
+      41.75972,
+      10.941944
+    ],
+    "max": [
+      43.42083,
+      12.708332
+    ],
+    "area": 2318,
+    "lat": 11.9,
+    "lon": 42.516,
+    "population": {
+      "2005": 804206
+    }
+  },
+  {
+    "name": "Dominica",
+    "min": [
+      -61.491394,
+      15.198055
+    ],
+    "max": [
+      -61.250557,
+      15.631943
+    ],
+    "area": 75,
+    "lat": 15.475,
+    "lon": -61.356,
+    "population": {
+      "2005": 67827
+    }
+  },
+  {
+    "name": "Dominican Republic",
+    "min": [
+      -72.00306699999999,
+      17.540276000000006
+    ],
+    "max": [
+      -68.32223499999992,
+      19.931110000000047
+    ],
+    "area": 4838,
+    "lat": 19.015,
+    "lon": -70.729,
+    "population": {
+      "2005": 9469601
+    }
+  },
+  {
+    "name": "Ecuador",
+    "min": [
+      -91.66389500000002,
+      -5.0091319999999655
+    ],
+    "max": [
+      -75.21608000000003,
+      1.4377779999999234
+    ],
+    "area": 27684,
+    "lat": -1.385,
+    "lon": -78.497,
+    "population": {
+      "2005": 13060993
+    }
+  },
+  {
+    "name": "Egypt",
+    "min": [
+      24.706664999999987,
+      21.994164000000126
+    ],
+    "max": [
+      36.898330999999985,
+      31.646941999999967
+    ],
+    "area": 99545,
+    "lat": 26.494,
+    "lon": 29.872,
+    "population": {
+      "2005": 72849793
+    }
+  },
+  {
+    "name": "Ireland",
+    "min": [
+      -10.47472399999998,
+      51.445549000000085
+    ],
+    "max": [
+      -6.013056000000006,
+      55.380272000000105
+    ],
+    "area": 6889,
+    "lat": 53.177,
+    "lon": -8.152,
+    "population": {
+      "2005": 4143294
+    }
+  },
+  {
+    "name": "Equatorial Guinea",
+    "min": [
+      5.615276999999992,
+      -1.4794449999999983
+    ],
+    "max": [
+      11.353887999999927,
+      3.763332999999932
+    ],
+    "area": 2805,
+    "lat": 1.607,
+    "lon": 10.488,
+    "population": {
+      "2005": 484098
+    }
+  },
+  {
+    "name": "Estonia",
+    "min": [
+      21.831939999999975,
+      57.522217000000126
+    ],
+    "max": [
+      28.19527400000004,
+      59.66832700000009
+    ],
+    "area": 4239,
+    "lat": 58.674,
+    "lon": 25.793,
+    "population": {
+      "2005": 1344312
+    }
+  },
+  {
+    "name": "Eritrea",
+    "min": [
+      36.44328300000012,
+      12.363888000000088
+    ],
+    "max": [
+      43.12138400000009,
+      17.99488100000002
+    ],
+    "area": 10100,
+    "lat": 16.045,
+    "lon": 38.219,
+    "population": {
+      "2005": 4526722
+    }
+  },
+  {
+    "name": "El Salvador",
+    "min": [
+      -90.108337,
+      13.156386999999995
+    ],
+    "max": [
+      -87.68472299999996,
+      14.431982000000062
+    ],
+    "area": 2072,
+    "lat": 13.736,
+    "lon": -88.866,
+    "population": {
+      "2005": 6668356
+    }
+  },
+  {
+    "name": "Ethiopia",
+    "min": [
+      32.991104,
+      3.406389
+    ],
+    "max": [
+      47.988243,
+      14.88361
+    ],
+    "area": 100000,
+    "lat": 8.626,
+    "lon": 39.616,
+    "population": {
+      "2005": 78985857
+    }
+  },
+  {
+    "name": "Austria",
+    "min": [
+      9.533569,
+      46.407494
+    ],
+    "max": [
+      17.166386,
+      49.018883
+    ],
+    "area": 8245,
+    "lat": 47.683,
+    "lon": 14.912,
+    "population": {
+      "2005": 8291979
+    }
+  },
+  {
+    "name": "Czech Republic",
+    "min": [
+      12.093704,
+      48.581379
+    ],
+    "max": [
+      18.852219,
+      51.053604
+    ],
+    "area": 7727,
+    "lat": 49.743,
+    "lon": 15.338,
+    "population": {
+      "2005": 10191762
+    }
+  },
+  {
+    "name": "French Guiana",
+    "min": [
+      -54.603783,
+      2.112222
+    ],
+    "max": [
+      -51.647781,
+      5.755555
+    ],
+    "area": 8815,
+    "lat": 3.924,
+    "lon": -53.241,
+    "population": {
+      "2005": 192099
+    }
+  },
+  {
+    "name": "Finland",
+    "min": [
+      20.580929000000026,
+      59.80499300000014
+    ],
+    "max": [
+      31.588928000000124,
+      70.08888200000007
+    ],
+    "area": 30459,
+    "lat": 64.504,
+    "lon": 26.272,
+    "population": {
+      "2005": 5246004
+    }
+  },
+  {
+    "name": "Fiji",
+    "min": [
+      -179.99999999999997,
+      -20.674441999999942
+    ],
+    "max": [
+      180,
+      -12.481942999999944
+    ],
+    "area": 1827,
+    "lat": -17.819,
+    "lon": 177.974,
+    "population": {
+      "2005": 828046
+    }
+  },
+  {
+    "name": "Falkland Islands (Malvinas)",
+    "min": [
+      -61.315833999999995,
+      -52.34305599999993
+    ],
+    "max": [
+      -57.73139199999997,
+      -51.249450999999965
+    ],
+    "area": 1217,
+    "lat": -51.665,
+    "lon": -58.694,
+    "population": {
+      "2005": 2975
+    }
+  },
+  {
+    "name": "Micronesia, Federated States of",
+    "min": [
+      138.0583190000001,
+      5.261666000000105
+    ],
+    "max": [
+      163.04330400000003,
+      9.589441000000079
+    ],
+    "area": 70,
+    "lat": 6.883,
+    "lon": 158.235,
+    "population": {
+      "2005": 110058
+    }
+  },
+  {
+    "name": "French Polynesia",
+    "min": [
+      -152.87972999999997,
+      -27.91555399999993
+    ],
+    "max": [
+      -134.94140599999997,
+      -7.888332999999989
+    ],
+    "area": 366,
+    "lat": -17.626,
+    "lon": -149.462,
+    "population": {
+      "2005": 255632
+    }
+  },
+  {
+    "name": "France",
+    "min": [
+      -5.134723000000008,
+      41.36416600000007
+    ],
+    "max": [
+      9.56222200000002,
+      51.09111000000007
+    ],
+    "area": 55010,
+    "lat": 46.565,
+    "lon": 2.55,
+    "population": {
+      "2005": 60990544
+    }
+  },
+  {
+    "name": "Gambia",
+    "min": [
+      -16.821667,
+      13.059977
+    ],
+    "max": [
+      -13.798613,
+      13.826387
+    ],
+    "area": 1000,
+    "lat": 13.453,
+    "lon": -15.386,
+    "population": {
+      "2005": 1617029
+    }
+  },
+  {
+    "name": "Gabon",
+    "min": [
+      8.698332000000107,
+      -3.925276999999994
+    ],
+    "max": [
+      14.520555000000115,
+      2.317898000000014
+    ],
+    "area": 25767,
+    "lat": -0.591,
+    "lon": 11.797,
+    "population": {
+      "2005": 1290693
+    }
+  },
+  {
+    "name": "Georgia",
+    "min": [
+      40.002968,
+      41.046097
+    ],
+    "max": [
+      46.710815,
+      43.584717
+    ],
+    "area": 6949,
+    "lat": 42.176,
+    "lon": 43.518,
+    "population": {
+      "2005": 4473409
+    }
+  },
+  {
+    "name": "Ghana",
+    "min": [
+      -3.249167,
+      4.726388
+    ],
+    "max": [
+      1.202778,
+      11.166666
+    ],
+    "area": 22754,
+    "lat": 7.96,
+    "lon": -1.207,
+    "population": {
+      "2005": 2253501
+    }
+  },
+  {
+    "name": "Grenada",
+    "min": [
+      -61.789725999999916,
+      11.996387000000027
+    ],
+    "max": [
+      -61.41861699999998,
+      12.529165000000035
+    ],
+    "area": 34,
+    "lat": 12.118,
+    "lon": -61.678,
+    "population": {
+      "2005": 105237
+    }
+  },
+  {
+    "name": "Greenland",
+    "min": [
+      -73.05360399999995,
+      59.79027600000012
+    ],
+    "max": [
+      -12.15500099999997,
+      83.62359600000008
+    ],
+    "area": 41045,
+    "lat": 74.719,
+    "lon": -41.391,
+    "population": {
+      "2005": 57475
+    }
+  },
+  {
+    "name": "Germany",
+    "min": [
+      5.8641660000000115,
+      47.27471900000006
+    ],
+    "max": [
+      15.038886999999988,
+      55.05666400000007
+    ],
+    "area": 34895,
+    "lat": 51.11,
+    "lon": 9.851,
+    "population": {
+      "2005": 82652369
+    }
+  },
+  {
+    "name": "Guam",
+    "min": [
+      144.634155,
+      13.234997
+    ],
+    "max": [
+      144.953308,
+      13.65361
+    ],
+    "area": 55,
+    "lat": 13.385,
+    "lon": 144.707,
+    "population": {
+      "2005": 16857
+    }
+  },
+  {
+    "name": "Greece",
+    "min": [
+      19.376110000000097,
+      34.808884000000035
+    ],
+    "max": [
+      28.238049000000046,
+      41.74832200000003
+    ],
+    "area": 12890,
+    "lat": 39.666,
+    "lon": 21.766,
+    "population": {
+      "2005": 11099737
+    }
+  },
+  {
+    "name": "Guatemala",
+    "min": [
+      -92.24678,
+      13.745832
+    ],
+    "max": [
+      -88.214737,
+      17.82111
+    ],
+    "area": 10843,
+    "lat": 15.256,
+    "lon": -90.398,
+    "population": {
+      "2005": 12709564
+    }
+  },
+  {
+    "name": "Guinea",
+    "min": [
+      -15.081112,
+      7.198889
+    ],
+    "max": [
+      -7.646536,
+      12.6775
+    ],
+    "area": 24572,
+    "lat": 10.439,
+    "lon": -10.942,
+    "population": {
+      "2005": 9002656
+    }
+  },
+  {
+    "name": "Guyana",
+    "min": [
+      -61.389725,
+      1.185555000000079
+    ],
+    "max": [
+      -56.47063399999996,
+      8.53527600000001
+    ],
+    "area": 19685,
+    "lat": 4.792,
+    "lon": -58.974,
+    "population": {
+      "2005": 739472
+    }
+  },
+  {
+    "name": "Haiti",
+    "min": [
+      -74.46778899999998,
+      18.022778000000017
+    ],
+    "max": [
+      -71.62889100000001,
+      20.092219999999998
+    ],
+    "area": 2756,
+    "lat": 19.142,
+    "lon": -72.278,
+    "population": {
+      "2005": 9296291
+    }
+  },
+  {
+    "name": "Honduras",
+    "min": [
+      -89.35195899999991,
+      12.97972100000004
+    ],
+    "max": [
+      -83.13185099999998,
+      17.420277
+    ],
+    "area": 11189,
+    "lat": 14.819,
+    "lon": -86.863,
+    "population": {
+      "2005": 683411
+    }
+  },
+  {
+    "name": "Croatia",
+    "min": [
+      13.496387000000084,
+      42.39666000000011
+    ],
+    "max": [
+      19.426109000000054,
+      46.535827999999924
+    ],
+    "area": 5592,
+    "lat": 45.723,
+    "lon": 16.693,
+    "population": {
+      "2005": 455149
+    }
+  },
+  {
+    "name": "Hungary",
+    "min": [
+      16.111805,
+      45.748329
+    ],
+    "max": [
+      22.894804,
+      48.57666
+    ],
+    "area": 9210,
+    "lat": 47.07,
+    "lon": 19.134,
+    "population": {
+      "2005": 10086387
+    }
+  },
+  {
+    "name": "Iceland",
+    "min": [
+      -24.542225,
+      63.389999
+    ],
+    "max": [
+      -13.499445,
+      66.536102
+    ],
+    "area": 10025,
+    "lat": 64.764,
+    "lon": -18.48,
+    "population": {
+      "2005": 295732
+    }
+  },
+  {
+    "name": "India",
+    "min": [
+      68.13943500000005,
+      6.745554000000027
+    ],
+    "max": [
+      97.380539,
+      35.50610399999999
+    ],
+    "area": 297319,
+    "lat": 21,
+    "lon": 78.5,
+    "population": {
+      "2005": 1134403141
+    }
+  },
+  {
+    "name": "Iran (Islamic Republic of)",
+    "min": [
+      44.03415700000005,
+      25.07527499999992
+    ],
+    "max": [
+      63.341934000000094,
+      39.78054000000003
+    ],
+    "area": 163620,
+    "lat": 32.565,
+    "lon": 54.301,
+    "population": {
+      "2005": 69420607
+    }
+  },
+  {
+    "name": "Israel",
+    "min": [
+      34.26757800000013,
+      29.486706000000083
+    ],
+    "max": [
+      35.68305200000003,
+      33.27027100000004
+    ],
+    "area": 2171,
+    "lat": 31.026,
+    "lon": 34.851,
+    "population": {
+      "2005": 6692037
+    }
+  },
+  {
+    "name": "Italy",
+    "min": [
+      6.619759999999985,
+      36.64916200000005
+    ],
+    "max": [
+      18.514999000000046,
+      47.09471899999994
+    ],
+    "area": 29411,
+    "lat": 42.7,
+    "lon": 12.8,
+    "population": {
+      "2005": 5864636
+    }
+  },
+  {
+    "name": "Cote d'Ivoire",
+    "min": [
+      -8.606383999999935,
+      4.34472199999999
+    ],
+    "max": [
+      -2.4877779999999348,
+      10.735255999999993
+    ],
+    "area": 31800,
+    "lat": 7.632,
+    "lon": -5.556,
+    "population": {
+      "2005": 18584701
+    }
+  },
+  {
+    "name": "Iraq",
+    "min": [
+      38.794701,
+      29.061661
+    ],
+    "max": [
+      48.563881,
+      37.38472
+    ],
+    "area": 43737,
+    "lat": 33.048,
+    "lon": 43.772,
+    "population": {
+      "2005": 27995984
+    }
+  },
+  {
+    "name": "Japan",
+    "min": [
+      122.93525700000009,
+      24.250832000000003
+    ],
+    "max": [
+      153.96579000000008,
+      45.486382000000106
+    ],
+    "area": 36450,
+    "lat": 36.491,
+    "lon": 139.068,
+    "population": {
+      "2005": 127896740
+    }
+  },
+  {
+    "name": "Jamaica",
+    "min": [
+      -78.373901,
+      17.696663
+    ],
+    "max": [
+      -76.221115,
+      18.522499
+    ],
+    "area": 1083,
+    "lat": 18.151,
+    "lon": -77.32,
+    "population": {
+      "2005": 2682469
+    }
+  },
+  {
+    "name": "Jordan",
+    "min": [
+      34.959999,
+      29.188889
+    ],
+    "max": [
+      39.301109,
+      33.377594
+    ],
+    "area": 8824,
+    "lat": 30.703,
+    "lon": 36.319,
+    "population": {
+      "2005": 5544066
+    }
+  },
+  {
+    "name": "Kenya",
+    "min": [
+      33.907219000000055,
+      -4.669617999999957
+    ],
+    "max": [
+      41.90516700000012,
+      4.622499000000005
+    ],
+    "area": 56914,
+    "lat": 0.53,
+    "lon": 37.858,
+    "population": {
+      "2005": 35598952
+    }
+  },
+  {
+    "name": "Kyrgyzstan",
+    "min": [
+      69.248871,
+      39.191856
+    ],
+    "max": [
+      80.283325,
+      43.216904
+    ],
+    "area": 19180,
+    "lat": 41.465,
+    "lon": 74.555,
+    "population": {
+      "2005": 5203547
+    }
+  },
+  {
+    "name": "Korea, Democratic People's Republic of",
+    "min": [
+      124.322769,
+      37.671379000000115
+    ],
+    "max": [
+      130.69741799999997,
+      43.00832400000013
+    ],
+    "area": 12041,
+    "lat": 39.778,
+    "lon": 126.451,
+    "population": {
+      "2005": 23615611
+    }
+  },
+  {
+    "name": "Kiribati",
+    "min": [
+      -172.233337,
+      -11.46666499999992
+    ],
+    "max": [
+      176.85024999999996,
+      4.725832000000025
+    ],
+    "area": 73,
+    "lat": -1.508,
+    "lon": 175.036,
+    "population": {
+      "2005": 92003
+    }
+  },
+  {
+    "name": "Korea, Republic of",
+    "min": [
+      124.60971100000006,
+      33.19026900000006
+    ],
+    "max": [
+      130.92413299999998,
+      38.625244000000066
+    ],
+    "area": 9873,
+    "lat": 36.504,
+    "lon": 128.103,
+    "population": {
+      "2005": 47869837
+    }
+  },
+  {
+    "name": "Kuwait",
+    "min": [
+      46.54694400000005,
+      28.538883
+    ],
+    "max": [
+      48.41658799999999,
+      30.084438000000034
+    ],
+    "area": 1782,
+    "lat": 29.476,
+    "lon": 47.376,
+    "population": {
+      "2005": 2700
+    }
+  },
+  {
+    "name": "Kazakhstan",
+    "min": [
+      46.49916100000013,
+      40.59443699999997
+    ],
+    "max": [
+      87.348206,
+      55.444709999999986
+    ],
+    "area": 269970,
+    "lat": 48.16,
+    "lon": 67.301,
+    "population": {
+      "2005": 15210609
+    }
+  },
+  {
+    "name": "Lao People's Democratic Republic",
+    "min": [
+      100.09137,
+      13.926664
+    ],
+    "max": [
+      107.695251,
+      22.500832
+    ],
+    "area": 23080,
+    "lat": 19.905,
+    "lon": 102.471,
+    "population": {
+      "2005": 566391
+    }
+  },
+  {
+    "name": "Lebanon",
+    "min": [
+      35.10083,
+      33.061943
+    ],
+    "max": [
+      36.623741,
+      34.647499
+    ],
+    "area": 1023,
+    "lat": 33.92,
+    "lon": 35.888,
+    "population": {
+      "2005": 401074
+    }
+  },
+  {
+    "name": "Latvia",
+    "min": [
+      20.968605,
+      55.674835
+    ],
+    "max": [
+      28.237774,
+      58.084435
+    ],
+    "area": 6205,
+    "lat": 56.858,
+    "lon": 25.641,
+    "population": {
+      "2005": 2301793
+    }
+  },
+  {
+    "name": "Belarus",
+    "min": [
+      23.1654,
+      51.251846
+    ],
+    "max": [
+      32.741379,
+      56.16777
+    ],
+    "area": 20748,
+    "lat": 53.54,
+    "lon": 28.047,
+    "population": {
+      "2005": 9795287
+    }
+  },
+  {
+    "name": "Lithuania",
+    "min": [
+      20.942833000000064,
+      53.88804600000009
+    ],
+    "max": [
+      26.81971700000014,
+      56.450829
+    ],
+    "area": 6268,
+    "lat": 55.336,
+    "lon": 23.897,
+    "population": {
+      "2005": 3425077
+    }
+  },
+  {
+    "name": "Liberia",
+    "min": [
+      -11.492331,
+      4.343333
+    ],
+    "max": [
+      -7.366667,
+      8.512777
+    ],
+    "area": 9632,
+    "lat": 6.682,
+    "lon": -9.657,
+    "population": {
+      "2005": 3441796
+    }
+  },
+  {
+    "name": "Slovakia",
+    "min": [
+      16.839996,
+      47.737221
+    ],
+    "max": [
+      22.558052,
+      49.60083
+    ],
+    "area": 4808,
+    "lat": 48.707,
+    "lon": 19.491,
+    "population": {
+      "2005": 5386995
+    }
+  },
+  {
+    "name": "Liechtenstein",
+    "min": [
+      9.474637,
+      47.057457
+    ],
+    "max": [
+      9.63611,
+      47.274544
+    ],
+    "area": 16,
+    "lat": 47.153,
+    "lon": 9.555,
+    "population": {
+      "2005": 34598
+    }
+  },
+  {
+    "name": "Libyan Arab Jamahiriya",
+    "min": [
+      9.303888,
+      19.499065
+    ],
+    "max": [
+      25.152775,
+      33.171135
+    ],
+    "area": 175954,
+    "lat": 27.044,
+    "lon": 18.023,
+    "population": {
+      "2005": 5918217
+    }
+  },
+  {
+    "name": "Madagascar",
+    "min": [
+      43.23682400000001,
+      -25.588337000000024
+    ],
+    "max": [
+      50.50138900000002,
+      -11.945557000000008
+    ],
+    "area": 58154,
+    "lat": -19.374,
+    "lon": 46.706,
+    "population": {
+      "2005": 18642586
+    }
+  },
+  {
+    "name": "Martinique",
+    "min": [
+      -61.231674,
+      14.402777
+    ],
+    "max": [
+      -60.816673,
+      14.880278
+    ],
+    "area": 106,
+    "lat": 14.653,
+    "lon": -61.021,
+    "population": {
+      "2005": 395896
+    }
+  },
+  {
+    "name": "Mongolia",
+    "min": [
+      87.758331,
+      41.581383
+    ],
+    "max": [
+      119.934982,
+      52.143608
+    ],
+    "area": 156650,
+    "lat": 46.056,
+    "lon": 102.876,
+    "population": {
+      "2005": 2580704
+    }
+  },
+  {
+    "name": "Montserrat",
+    "min": [
+      -62.237228,
+      16.671387
+    ],
+    "max": [
+      -62.137505,
+      16.81361
+    ],
+    "area": 10,
+    "lat": 16.736,
+    "lon": -62.187,
+    "population": {
+      "2005": 5628
+    }
+  },
+  {
+    "name": "The former Yugoslav Republic of Macedonia",
+    "min": [
+      20.457775,
+      40.855888
+    ],
+    "max": [
+      23.032776,
+      42.361382
+    ],
+    "area": 2543,
+    "lat": 41.6,
+    "lon": 21.698,
+    "population": {
+      "2005": 2033655
+    }
+  },
+  {
+    "name": "Mali",
+    "min": [
+      -12.244833,
+      10.141109
+    ],
+    "max": [
+      4.2525,
+      25.000275
+    ],
+    "area": 122019,
+    "lat": 17.35,
+    "lon": -3.524,
+    "population": {
+      "2005": 1161109
+    }
+  },
+  {
+    "name": "Morocco",
+    "min": [
+      -13.174961,
+      27.664238
+    ],
+    "max": [
+      -1.010278,
+      35.919167
+    ],
+    "area": 44630,
+    "lat": 32.706,
+    "lon": -5.758,
+    "population": {
+      "2005": 30494991
+    }
+  },
+  {
+    "name": "Mauritius",
+    "min": [
+      56.50721699999997,
+      -20.52055699999994
+    ],
+    "max": [
+      63.49860400000006,
+      -10.316667999999993
+    ],
+    "area": 203,
+    "lat": -20.255,
+    "lon": 57.583,
+    "population": {
+      "2005": 1241173
+    }
+  },
+  {
+    "name": "Mauritania",
+    "min": [
+      -17.075557999999944,
+      14.725321000000008
+    ],
+    "max": [
+      -4.806110999999987,
+      27.290459000000055
+    ],
+    "area": 102522,
+    "lat": 20.26,
+    "lon": -10.332,
+    "population": {
+      "2005": 2963105
+    }
+  },
+  {
+    "name": "Malta",
+    "min": [
+      14.18083200000001,
+      35.79999500000008
+    ],
+    "max": [
+      14.57000000000005,
+      36.074996999999996
+    ],
+    "area": 32,
+    "lat": 35.89,
+    "lon": 14.442,
+    "population": {
+      "2005": 402617
+    }
+  },
+  {
+    "name": "Oman",
+    "min": [
+      51.99928999999992,
+      16.64277800000002
+    ],
+    "max": [
+      59.84722099999999,
+      26.382389000000046
+    ],
+    "area": 30950,
+    "lat": 21.656,
+    "lon": 57.407,
+    "population": {
+      "2005": 2507042
+    }
+  },
+  {
+    "name": "Maldives",
+    "min": [
+      72.68775900000003,
+      -0.6908329999999978
+    ],
+    "max": [
+      73.75360100000006,
+      7.09638799999999
+    ],
+    "area": 30,
+    "lat": 3.548,
+    "lon": 72.92,
+    "population": {
+      "2005": 295297
+    }
+  },
+  {
+    "name": "Mexico",
+    "min": [
+      -118.40416699999997,
+      14.550547000000108
+    ],
+    "max": [
+      -86.70140099999998,
+      32.718456
+    ],
+    "area": 190869,
+    "lat": 23.951,
+    "lon": -102.535,
+    "population": {
+      "2005": 104266392
+    }
+  },
+  {
+    "name": "Malaysia",
+    "min": [
+      99.64082300000013,
+      0.8527780000000575
+    ],
+    "max": [
+      119.27581799999996,
+      7.353610000000117
+    ],
+    "area": 32855,
+    "lat": 4.201,
+    "lon": 102.195,
+    "population": {
+      "2005": 25652985
+    }
+  },
+  {
+    "name": "Mozambique",
+    "min": [
+      30.213017000000093,
+      -26.86027899999999
+    ],
+    "max": [
+      40.846107000000075,
+      -10.471111000000008
+    ],
+    "area": 78409,
+    "lat": -14.422,
+    "lon": 37.923,
+    "population": {
+      "2005": 20532675
+    }
+  },
+  {
+    "name": "Malawi",
+    "min": [
+      32.67888599999998,
+      -17.13528100000002
+    ],
+    "max": [
+      35.92416400000013,
+      -9.373334999999997
+    ],
+    "area": 9408,
+    "lat": -13.4,
+    "lon": 33.808,
+    "population": {
+      "2005": 13226091
+    }
+  },
+  {
+    "name": "New Caledonia",
+    "min": [
+      159.92221100000006,
+      -22.694163999999944
+    ],
+    "max": [
+      171.31387299999994,
+      -19.114444999999932
+    ],
+    "area": 1828,
+    "lat": -21.359,
+    "lon": 165.447,
+    "population": {
+      "2005": 234185
+    }
+  },
+  {
+    "name": "Niue",
+    "min": [
+      -169.953064,
+      -19.145557
+    ],
+    "max": [
+      -169.781403,
+      -18.963333
+    ],
+    "area": 26,
+    "lat": -19.052,
+    "lon": -169.869,
+    "population": {
+      "2005": 1632
+    }
+  },
+  {
+    "name": "Niger",
+    "min": [
+      0.166667,
+      11.693274
+    ],
+    "max": [
+      15.996666,
+      23.522305
+    ],
+    "area": 126670,
+    "lat": 17.426,
+    "lon": 9.398,
+    "population": {
+      "2005": 1326419
+    }
+  },
+  {
+    "name": "Aruba",
+    "min": [
+      -70.063339,
+      12.41111
+    ],
+    "max": [
+      -69.873337,
+      12.631109
+    ],
+    "area": 0,
+    "lat": 12.517,
+    "lon": -69.977,
+    "population": {
+      "2005": 102897
+    }
+  },
+  {
+    "name": "Anguilla",
+    "min": [
+      -63.167778,
+      18.164444
+    ],
+    "max": [
+      -62.969452,
+      18.276665
+    ],
+    "area": 0,
+    "lat": 18.237,
+    "lon": -63.032,
+    "population": {
+      "2005": 12256
+    }
+  },
+  {
+    "name": "Belgium",
+    "min": [
+      2.541667,
+      49.504166
+    ],
+    "max": [
+      6.398204,
+      51.503609
+    ],
+    "area": 0,
+    "lat": 50.643,
+    "lon": 4.664,
+    "population": {
+      "2005": 10398049
+    }
+  },
+  {
+    "name": "Hong Kong",
+    "min": [
+      113.822769,
+      22.1936070000001
+    ],
+    "max": [
+      114.39026600000011,
+      22.55054824689705
+    ],
+    "area": 0,
+    "lat": 22.423,
+    "lon": 114.129,
+    "population": {
+      "2005": 7057418
+    }
+  },
+  {
+    "name": "Northern Mariana Islands",
+    "min": [
+      144.89859000000013,
+      14.105276000000003
+    ],
+    "max": [
+      145.87078900000006,
+      20.556384999999977
+    ],
+    "area": 0,
+    "lat": 15.005,
+    "lon": 145.623,
+    "population": {
+      "2005": 80258
+    }
+  },
+  {
+    "name": "Faroe Islands",
+    "min": [
+      -7.435000000000002,
+      61.38832899999994
+    ],
+    "max": [
+      -6.388612000000023,
+      62.39694200000008
+    ],
+    "area": 0,
+    "lat": 62.05,
+    "lon": -6.864,
+    "population": {
+      "2005": 48205
+    }
+  },
+  {
+    "name": "Andorra",
+    "min": [
+      1.421389,
+      42.436104
+    ],
+    "max": [
+      1.78172,
+      42.656387
+    ],
+    "area": 0,
+    "lat": 42.549,
+    "lon": 1.576,
+    "population": {
+      "2005": 73483
+    }
+  },
+  {
+    "name": "Gibraltar",
+    "min": [
+      -5.35624,
+      36.112175
+    ],
+    "max": [
+      -5.334508,
+      36.163307
+    ],
+    "area": 0,
+    "lat": 36.138,
+    "lon": -5.345,
+    "population": {
+      "2005": 291
+    }
+  },
+  {
+    "name": "Isle of Man",
+    "min": [
+      -4.788611,
+      54.05555
+    ],
+    "max": [
+      -4.307501,
+      54.416664
+    ],
+    "area": 0,
+    "lat": 54.229,
+    "lon": -4.527,
+    "population": {
+      "2005": 78357
+    }
+  },
+  {
+    "name": "Luxembourg",
+    "min": [
+      5.734444,
+      49.448326
+    ],
+    "max": [
+      6.524722,
+      50.18222
+    ],
+    "area": 0,
+    "lat": 49.771,
+    "lon": 6.088,
+    "population": {
+      "2005": 456613
+    }
+  },
+  {
+    "name": "Macau",
+    "min": [
+      113.531372,
+      22.183052
+    ],
+    "max": [
+      113.556374,
+      22.214439
+    ],
+    "area": 0,
+    "lat": 22.2,
+    "lon": 113.545,
+    "population": {
+      "2005": 47309
+    }
+  },
+  {
+    "name": "Monaco",
+    "min": [
+      7.386389,
+      43.727547
+    ],
+    "max": [
+      7.439293,
+      43.773048
+    ],
+    "area": 0,
+    "lat": 43.75,
+    "lon": 7.412,
+    "population": {
+      "2005": 325
+    }
+  },
+  {
+    "name": "Palestine",
+    "min": [
+      34.21665999999999,
+      31.21654099999995
+    ],
+    "max": [
+      35.57329600000003,
+      32.546387000000095
+    ],
+    "area": 0,
+    "lat": 32.037,
+    "lon": 35.278,
+    "population": {
+      "2005": 3762005
+    }
+  },
+  {
+    "name": "Montenegro",
+    "min": [
+      18.453331,
+      41.848999
+    ],
+    "max": [
+      20.382774,
+      43.556107
+    ],
+    "area": 0,
+    "lat": 42.792,
+    "lon": 19.254,
+    "population": {
+      "2005": 607969
+    }
+  },
+  {
+    "name": "Mayotte",
+    "min": [
+      45.03916200000003,
+      -12.992500000000007
+    ],
+    "max": [
+      45.29332700000003,
+      -12.662499999999966
+    ],
+    "area": 0,
+    "lat": -12.777,
+    "lon": 45.155,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Åland Islands",
+    "min": [
+      19.51055500000001,
+      59.97694400000006
+    ],
+    "max": [
+      20.44249700000006,
+      60.40361000000013
+    ],
+    "area": 0,
+    "lat": 60.198,
+    "lon": 19.952,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Norfolk Island",
+    "min": [
+      167.909424,
+      -29.081112
+    ],
+    "max": [
+      168,
+      -29.000557
+    ],
+    "area": 0,
+    "lat": -29.037,
+    "lon": 167.953,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Cocos (Keeling) Islands",
+    "min": [
+      96.81749000000002,
+      -12.199998999999991
+    ],
+    "max": [
+      96.92442300000005,
+      -12.128331999999943
+    ],
+    "area": 1,
+    "lat": -12.173,
+    "lon": 96.839,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Antarctica",
+    "min": [
+      -179.99999999999997,
+      -90
+    ],
+    "max": [
+      180,
+      -60.50194499999992
+    ],
+    "area": 0,
+    "lat": -80.446,
+    "lon": 21.304,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Bouvet Island",
+    "min": [
+      3.341666,
+      -54.462784
+    ],
+    "max": [
+      3.484722,
+      -54.383614
+    ],
+    "area": 0,
+    "lat": -54.422,
+    "lon": 3.412,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "French Southern and Antarctic Lands",
+    "min": [
+      42.72110700000013,
+      -49.72500599999995
+    ],
+    "max": [
+      77.58888199999996,
+      -17.05111299999993
+    ],
+    "area": 0,
+    "lat": -49.302,
+    "lon": 69.117,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Heard Island and McDonald Islands",
+    "min": [
+      73.234436,
+      -53.199448
+    ],
+    "max": [
+      73.77388,
+      -52.964172
+    ],
+    "area": 0,
+    "lat": -53.111,
+    "lon": 73.507,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "British Indian Ocean Territory",
+    "min": [
+      71.25860600000004,
+      -7.436666000000002
+    ],
+    "max": [
+      72.49443100000008,
+      -5.24972200000002
+    ],
+    "area": 0,
+    "lat": -7.335,
+    "lon": 72.416,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Christmas Island",
+    "min": [
+      105.628998,
+      -10.51097
+    ],
+    "max": [
+      105.7519,
+      -10.38408
+    ],
+    "area": 0,
+    "lat": -10.444,
+    "lon": 105.704,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "United States Minor Outlying Islands",
+    "min": [
+      -177.395844,
+      -0.39805599999994
+    ],
+    "max": [
+      166.66247600000008,
+      28.221934999999917
+    ],
+    "area": 0,
+    "lat": -0.385,
+    "lon": -160.027,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Vanuatu",
+    "min": [
+      166.5166630000001,
+      -20.254168999999933
+    ],
+    "max": [
+      170.23522900000012,
+      -13.070555000000013
+    ],
+    "area": 1219,
+    "lat": -15.376,
+    "lon": 166.899,
+    "population": {
+      "2005": 215366
+    }
+  },
+  {
+    "name": "Nigeria",
+    "min": [
+      2.6924999999999386,
+      4.272499000000096
+    ],
+    "max": [
+      14.658053999999993,
+      13.891499000000124
+    ],
+    "area": 91077,
+    "lat": 9.594,
+    "lon": 8.105,
+    "population": {
+      "2005": 141356083
+    }
+  },
+  {
+    "name": "Netherlands",
+    "min": [
+      3.370865999999978,
+      50.75388300000009
+    ],
+    "max": [
+      7.21166599999998,
+      53.51138300000014
+    ],
+    "area": 3388,
+    "lat": 52.077,
+    "lon": 5.389,
+    "population": {
+      "2005": 1632769
+    }
+  },
+  {
+    "name": "Norway",
+    "min": [
+      4.6200000000000045,
+      57.987777999999935
+    ],
+    "max": [
+      31.07805300000001,
+      71.15470900000008
+    ],
+    "area": 30625,
+    "lat": 61.152,
+    "lon": 8.74,
+    "population": {
+      "2005": 4638836
+    }
+  },
+  {
+    "name": "Nepal",
+    "min": [
+      80.0522,
+      26.364719
+    ],
+    "max": [
+      88.195816,
+      30.424995
+    ],
+    "area": 14300,
+    "lat": 28.253,
+    "lon": 83.939,
+    "population": {
+      "2005": 27093656
+    }
+  },
+  {
+    "name": "Nauru",
+    "min": [
+      166.904419,
+      -0.552222
+    ],
+    "max": [
+      166.958588,
+      -0.493333
+    ],
+    "area": 2,
+    "lat": -0.522,
+    "lon": 166.93,
+    "population": {
+      "2005": 10111
+    }
+  },
+  {
+    "name": "Suriname",
+    "min": [
+      -58.071396,
+      1.835556
+    ],
+    "max": [
+      -53.984169,
+      6.003055
+    ],
+    "area": 15600,
+    "lat": 4.127,
+    "lon": -55.912,
+    "population": {
+      "2005": 452468
+    }
+  },
+  {
+    "name": "Nicaragua",
+    "min": [
+      -87.69306899999998,
+      10.708611000000019
+    ],
+    "max": [
+      -82.72138999999993,
+      15.022221000000002
+    ],
+    "area": 12140,
+    "lat": 12.84,
+    "lon": -85.034,
+    "population": {
+      "2005": 5462539
+    }
+  },
+  {
+    "name": "New Zealand",
+    "min": [
+      -178.613068,
+      -52.578056000000004
+    ],
+    "max": [
+      179.08273299999996,
+      -29.223056999999983
+    ],
+    "area": 26799,
+    "lat": -42.634,
+    "lon": 172.235,
+    "population": {
+      "2005": 4097112
+    }
+  },
+  {
+    "name": "Paraguay",
+    "min": [
+      -62.643768,
+      -27.588337
+    ],
+    "max": [
+      -54.243896,
+      -19.296669
+    ],
+    "area": 39730,
+    "lat": -23.236,
+    "lon": -58.391,
+    "population": {
+      "2005": 5904342
+    }
+  },
+  {
+    "name": "Peru",
+    "min": [
+      -81.3564,
+      -18.348545
+    ],
+    "max": [
+      -68.673904,
+      -0.031389
+    ],
+    "area": 128000,
+    "lat": -9.326,
+    "lon": -75.552,
+    "population": {
+      "2005": 27274266
+    }
+  },
+  {
+    "name": "Pakistan",
+    "min": [
+      60.866302000000076,
+      23.688048999999978
+    ],
+    "max": [
+      77.82392900000013,
+      37.06259200000011
+    ],
+    "area": 77088,
+    "lat": 29.967,
+    "lon": 69.386,
+    "population": {
+      "2005": 158080591
+    }
+  },
+  {
+    "name": "Poland",
+    "min": [
+      14.145555,
+      49.001938
+    ],
+    "max": [
+      24.144718,
+      54.836937
+    ],
+    "area": 30629,
+    "lat": 52.125,
+    "lon": 19.401,
+    "population": {
+      "2005": 38195558
+    }
+  },
+  {
+    "name": "Panama",
+    "min": [
+      -83.03028899999993,
+      7.206111000000078
+    ],
+    "max": [
+      -77.19833399999999,
+      9.620277000000044
+    ],
+    "area": 7443,
+    "lat": 8.384,
+    "lon": -80.92,
+    "population": {
+      "2005": 3231502
+    }
+  },
+  {
+    "name": "Portugal",
+    "min": [
+      -31.29000099999996,
+      32.63749700000011
+    ],
+    "max": [
+      -6.187221999999963,
+      42.152739999999994
+    ],
+    "area": 9150,
+    "lat": 40.309,
+    "lon": -8.058,
+    "population": {
+      "2005": 10528226
+    }
+  },
+  {
+    "name": "Papua New Guinea",
+    "min": [
+      140.85885599999995,
+      -11.642499999999927
+    ],
+    "max": [
+      159.52304100000003,
+      -1.098333000000025
+    ],
+    "area": 45286,
+    "lat": -5.949,
+    "lon": 143.459,
+    "population": {
+      "2005": 6069715
+    }
+  },
+  {
+    "name": "Guinea-Bissau",
+    "min": [
+      -16.717769999999973,
+      10.92277699999994
+    ],
+    "max": [
+      -13.643056999999999,
+      12.684721000000025
+    ],
+    "area": 2812,
+    "lat": 12.125,
+    "lon": -14.651,
+    "population": {
+      "2005": 1596929
+    }
+  },
+  {
+    "name": "Qatar",
+    "min": [
+      50.751938,
+      24.556042
+    ],
+    "max": [
+      51.615829,
+      26.15361
+    ],
+    "area": 1100,
+    "lat": 25.316,
+    "lon": 51.191,
+    "population": {
+      "2005": 796186
+    }
+  },
+  {
+    "name": "Reunion",
+    "min": [
+      55.219719,
+      -21.37389
+    ],
+    "max": [
+      55.85305,
+      -20.856392
+    ],
+    "area": 250,
+    "lat": -21.122,
+    "lon": 55.538,
+    "population": {
+      "2005": 785159
+    }
+  },
+  {
+    "name": "Romania",
+    "min": [
+      20.261024,
+      43.622437
+    ],
+    "max": [
+      29.672497,
+      48.263885
+    ],
+    "area": 22987,
+    "lat": 45.844,
+    "lon": 24.969,
+    "population": {
+      "2005": 21627557
+    }
+  },
+  {
+    "name": "Republic of Moldova",
+    "min": [
+      26.634995,
+      45.448647
+    ],
+    "max": [
+      30.133228,
+      48.468323
+    ],
+    "area": 3288,
+    "lat": 47.193,
+    "lon": 28.599,
+    "population": {
+      "2005": 3876661
+    }
+  },
+  {
+    "name": "Philippines",
+    "min": [
+      116.94999700000017,
+      4.641388000000063
+    ],
+    "max": [
+      126.59803799999997,
+      21.118052999999918
+    ],
+    "area": 29817,
+    "lat": 11.118,
+    "lon": 122.466,
+    "population": {
+      "2005": 84566163
+    }
+  },
+  {
+    "name": "Puerto Rico",
+    "min": [
+      -67.93833899999998,
+      17.92222200000009
+    ],
+    "max": [
+      -65.24195900000001,
+      18.519444000000135
+    ],
+    "area": 887,
+    "lat": 18.221,
+    "lon": -66.466,
+    "population": {
+      "2005": 3946779
+    }
+  },
+  {
+    "name": "Russia",
+    "min": [
+      -179.99999999999997,
+      41.196091000000024
+    ],
+    "max": [
+      180,
+      81.85192900000004
+    ],
+    "area": 1638094,
+    "lat": 61.988,
+    "lon": 96.689,
+    "population": {
+      "2005": 143953092
+    }
+  },
+  {
+    "name": "Rwanda",
+    "min": [
+      28.853333,
+      -2.826667
+    ],
+    "max": [
+      30.894444,
+      -1.053889
+    ],
+    "area": 2467,
+    "lat": -1.998,
+    "lon": 29.917,
+    "population": {
+      "2005": 9233793
+    }
+  },
+  {
+    "name": "Saudi Arabia",
+    "min": [
+      34.49221800000004,
+      15.616942999999935
+    ],
+    "max": [
+      55.666106999999954,
+      32.15494200000012
+    ],
+    "area": 214969,
+    "lat": 24.023,
+    "lon": 44.585,
+    "population": {
+      "2005": 2361236
+    }
+  },
+  {
+    "name": "Saint Kitts and Nevis",
+    "min": [
+      -62.86389200000002,
+      17.091662999999983
+    ],
+    "max": [
+      -62.534171999999955,
+      17.41083100000003
+    ],
+    "area": 36,
+    "lat": 17.34,
+    "lon": -62.769,
+    "population": {
+      "2005": 49138
+    }
+  },
+  {
+    "name": "Seychelles",
+    "min": [
+      46.20416300000011,
+      -9.755000999999993
+    ],
+    "max": [
+      56.28611000000001,
+      -4.280001000000027
+    ],
+    "area": 46,
+    "lat": -4.647,
+    "lon": 55.474,
+    "population": {
+      "2005": 85532
+    }
+  },
+  {
+    "name": "South Africa",
+    "min": [
+      16.48333000000008,
+      -46.96972699999998
+    ],
+    "max": [
+      37.98166700000013,
+      -22.136391000000003
+    ],
+    "area": 121447,
+    "lat": -30.558,
+    "lon": 23.121,
+    "population": {
+      "2005": 47938663
+    }
+  },
+  {
+    "name": "Lesotho",
+    "min": [
+      27.011108,
+      -30.650528
+    ],
+    "max": [
+      29.456108,
+      -28.569447
+    ],
+    "area": 3035,
+    "lat": -29.581,
+    "lon": 28.243,
+    "population": {
+      "2005": 1980831
+    }
+  },
+  {
+    "name": "Botswana",
+    "min": [
+      19.996109,
+      -26.875557
+    ],
+    "max": [
+      29.373623,
+      -17.781391
+    ],
+    "area": 56673,
+    "lat": -22.182,
+    "lon": 23.815,
+    "population": {
+      "2005": 1835938
+    }
+  },
+  {
+    "name": "Senegal",
+    "min": [
+      -17.537224,
+      12.301748
+    ],
+    "max": [
+      -11.3675,
+      16.693054
+    ],
+    "area": 19253,
+    "lat": 15.013,
+    "lon": -14.881,
+    "population": {
+      "2005": 1177034
+    }
+  },
+  {
+    "name": "Slovenia",
+    "min": [
+      13.383055,
+      45.425819
+    ],
+    "max": [
+      16.607872,
+      46.876663
+    ],
+    "area": 2014,
+    "lat": 46.124,
+    "lon": 14.827,
+    "population": {
+      "2005": 1999425
+    }
+  },
+  {
+    "name": "Sierra Leone",
+    "min": [
+      -13.29561000000001,
+      6.923610999999937
+    ],
+    "max": [
+      -10.264167999999927,
+      9.997499000000062
+    ],
+    "area": 7162,
+    "lat": 8.56,
+    "lon": -11.792,
+    "population": {
+      "2005": 5586403
+    }
+  },
+  {
+    "name": "Singapore",
+    "min": [
+      103.640808,
+      1.258889
+    ],
+    "max": [
+      103.998863,
+      1.445277
+    ],
+    "area": 67,
+    "lat": 1.351,
+    "lon": 103.808,
+    "population": {
+      "2005": 4327468
+    }
+  },
+  {
+    "name": "Somalia",
+    "min": [
+      40.986595,
+      -1.674868
+    ],
+    "max": [
+      51.412636,
+      11.979166
+    ],
+    "area": 62734,
+    "lat": 9.774,
+    "lon": 48.316,
+    "population": {
+      "2005": 8196395
+    }
+  },
+  {
+    "name": "Spain",
+    "min": [
+      -18.17055899999997,
+      27.637496999999996
+    ],
+    "max": [
+      4.316944000000092,
+      43.77221700000001
+    ],
+    "area": 49904,
+    "lat": 40.227,
+    "lon": -3.649,
+    "population": {
+      "2005": 43397491
+    }
+  },
+  {
+    "name": "Saint Lucia",
+    "min": [
+      -61.079727,
+      13.709444
+    ],
+    "max": [
+      -60.878059,
+      14.109444
+    ],
+    "area": 61,
+    "lat": 13.898,
+    "lon": -60.969,
+    "population": {
+      "2005": 16124
+    }
+  },
+  {
+    "name": "Sudan",
+    "min": [
+      21.827774000000034,
+      3.493394000000137
+    ],
+    "max": [
+      38.607498000000135,
+      22.232219999999984
+    ],
+    "area": 237600,
+    "lat": 13.832,
+    "lon": 30.05,
+    "population": {
+      "2005": 36899747
+    }
+  },
+  {
+    "name": "Sweden",
+    "min": [
+      11.106943000000001,
+      55.339165000000094
+    ],
+    "max": [
+      24.16861,
+      69.06030299999992
+    ],
+    "area": 41033,
+    "lat": 62.011,
+    "lon": 15.27,
+    "population": {
+      "2005": 9038049
+    }
+  },
+  {
+    "name": "Syrian Arab Republic",
+    "min": [
+      35.614464,
+      32.313606
+    ],
+    "max": [
+      42.379166,
+      37.290543
+    ],
+    "area": 18378,
+    "lat": 35.013,
+    "lon": 38.506,
+    "population": {
+      "2005": 18893881
+    }
+  },
+  {
+    "name": "Switzerland",
+    "min": [
+      5.96611,
+      45.829437
+    ],
+    "max": [
+      10.488913,
+      47.806938
+    ],
+    "area": 4000,
+    "lat": 46.861,
+    "lon": 7.908,
+    "population": {
+      "2005": 7424389
+    }
+  },
+  {
+    "name": "Trinidad and Tobago",
+    "min": [
+      -61.92444599999999,
+      10.037498000000085
+    ],
+    "max": [
+      -60.52056099999993,
+      11.34610900000007
+    ],
+    "area": 513,
+    "lat": 10.468,
+    "lon": -61.253,
+    "population": {
+      "2005": 1323722
+    }
+  },
+  {
+    "name": "Thailand",
+    "min": [
+      97.345261,
+      5.631109999999978
+    ],
+    "max": [
+      105.63942700000013,
+      20.45527300000009
+    ],
+    "area": 51089,
+    "lat": 15.7,
+    "lon": 100.844,
+    "population": {
+      "2005": 63002911
+    }
+  },
+  {
+    "name": "Tajikistan",
+    "min": [
+      67.3647,
+      36.671844
+    ],
+    "max": [
+      75.187485,
+      41.050224
+    ],
+    "area": 13996,
+    "lat": 38.665,
+    "lon": 69.42,
+    "population": {
+      "2005": 6550213
+    }
+  },
+  {
+    "name": "Tokelau",
+    "min": [
+      -172.50033599999995,
+      -9.381110999999976
+    ],
+    "max": [
+      -171.21142599999996,
+      -8.553613999999925
+    ],
+    "area": 1,
+    "lat": -9.193,
+    "lon": -171.853,
+    "population": {
+      "2005": 1401
+    }
+  },
+  {
+    "name": "Tonga",
+    "min": [
+      -175.684723,
+      -21.454165999999987
+    ],
+    "max": [
+      -173.90615800000003,
+      -15.56027999999992
+    ],
+    "area": 72,
+    "lat": -21.202,
+    "lon": -175.185,
+    "population": {
+      "2005": 99361
+    }
+  },
+  {
+    "name": "Togo",
+    "min": [
+      -0.149762,
+      6.100546
+    ],
+    "max": [
+      1.799327,
+      11.13854
+    ],
+    "area": 5439,
+    "lat": 8.799,
+    "lon": 1.081,
+    "population": {
+      "2005": 6238572
+    }
+  },
+  {
+    "name": "Sao Tome and Principe",
+    "min": [
+      6.464444000000128,
+      0.018332999999984168
+    ],
+    "max": [
+      7.464166999999918,
+      1.7019440000001396
+    ],
+    "area": 96,
+    "lat": 0.201,
+    "lon": 6.629,
+    "population": {
+      "2005": 152622
+    }
+  },
+  {
+    "name": "Tunisia",
+    "min": [
+      7.491666000000066,
+      30.234390000000133
+    ],
+    "max": [
+      11.583331999999928,
+      37.539444
+    ],
+    "area": 15536,
+    "lat": 35.383,
+    "lon": 9.596,
+    "population": {
+      "2005": 10104685
+    }
+  },
+  {
+    "name": "Turkey",
+    "min": [
+      25.663883000000112,
+      35.81749700000012
+    ],
+    "max": [
+      44.82276200000001,
+      42.109993000000145
+    ],
+    "area": 76963,
+    "lat": 39.061,
+    "lon": 35.179,
+    "population": {
+      "2005": 72969723
+    }
+  },
+  {
+    "name": "Tuvalu",
+    "min": [
+      176.0663760000001,
+      -8.56129199999998
+    ],
+    "max": [
+      179.23228499999993,
+      -5.657777999999951
+    ],
+    "area": 3,
+    "lat": -8.514,
+    "lon": 179.219,
+    "population": {
+      "2005": 10441
+    }
+  },
+  {
+    "name": "Turkmenistan",
+    "min": [
+      52.4400710000001,
+      35.14166300000005
+    ],
+    "max": [
+      66.67248500000005,
+      42.797775000000115
+    ],
+    "area": 46993,
+    "lat": 39.122,
+    "lon": 59.384,
+    "population": {
+      "2005": 4833266
+    }
+  },
+  {
+    "name": "United Republic of Tanzania",
+    "min": [
+      29.340832000000034,
+      -11.740834999999947
+    ],
+    "max": [
+      40.436813000000086,
+      -0.9972219999999652
+    ],
+    "area": 88359,
+    "lat": -6.27,
+    "lon": 34.823,
+    "population": {
+      "2005": 38477873
+    }
+  },
+  {
+    "name": "Uganda",
+    "min": [
+      29.570831,
+      -1.47611
+    ],
+    "max": [
+      35.00972,
+      4.222777
+    ],
+    "area": 19710,
+    "lat": 1.28,
+    "lon": 32.386,
+    "population": {
+      "2005": 28947181
+    }
+  },
+  {
+    "name": "United Kingdom",
+    "min": [
+      -8.621388999999965,
+      49.9116590000001
+    ],
+    "max": [
+      1.7494439999999258,
+      60.84444400000007
+    ],
+    "area": 24193,
+    "lat": 53,
+    "lon": -1.6,
+    "population": {
+      "2005": 60244834
+    }
+  },
+  {
+    "name": "Ukraine",
+    "min": [
+      22.151441999999918,
+      44.379150000000095
+    ],
+    "max": [
+      40.17971800000004,
+      52.37971500000009
+    ],
+    "area": 57935,
+    "lat": 49.016,
+    "lon": 31.388,
+    "population": {
+      "2005": 46917544
+    }
+  },
+  {
+    "name": "United States",
+    "min": [
+      -179.14199799999994,
+      18.923882000000106
+    ],
+    "max": [
+      179.77746600000012,
+      71.36581400000006
+    ],
+    "area": 915896,
+    "lat": 39.622,
+    "lon": -98.606,
+    "population": {
+      "2005": 299846449
+    }
+  },
+  {
+    "name": "Burkina Faso",
+    "min": [
+      -5.521111,
+      9.393888
+    ],
+    "max": [
+      2.397925,
+      15.082777
+    ],
+    "area": 27360,
+    "lat": 12.278,
+    "lon": -1.74,
+    "population": {
+      "2005": 13933363
+    }
+  },
+  {
+    "name": "Uruguay",
+    "min": [
+      -58.438614,
+      -34.948891
+    ],
+    "max": [
+      -53.093056,
+      -30.096668
+    ],
+    "area": 17502,
+    "lat": -32.8,
+    "lon": -56.012,
+    "population": {
+      "2005": 3325727
+    }
+  },
+  {
+    "name": "Uzbekistan",
+    "min": [
+      55.99749,
+      37.183876
+    ],
+    "max": [
+      73.173035,
+      45.571106
+    ],
+    "area": 42540,
+    "lat": 41.75,
+    "lon": 63.17,
+    "population": {
+      "2005": 26593123
+    }
+  },
+  {
+    "name": "Saint Vincent and the Grenadines",
+    "min": [
+      -61.45416999999992,
+      12.584444000000076
+    ],
+    "max": [
+      -61.120285000000024,
+      13.384164999999996
+    ],
+    "area": 39,
+    "lat": 13.248,
+    "lon": -61.194,
+    "population": {
+      "2005": 119137
+    }
+  },
+  {
+    "name": "Venezuela",
+    "min": [
+      -73.37806699999999,
+      0.6486110000000167
+    ],
+    "max": [
+      -59.80139200000002,
+      12.198889000000008
+    ],
+    "area": 88205,
+    "lat": 7.125,
+    "lon": -66.166,
+    "population": {
+      "2005": 26725573
+    }
+  },
+  {
+    "name": "British Virgin Islands",
+    "min": [
+      -64.70028699999995,
+      18.38388800000007
+    ],
+    "max": [
+      -64.26917999999995,
+      18.748608000000047
+    ],
+    "area": 15,
+    "lat": 18.483,
+    "lon": -64.39,
+    "population": {
+      "2005": 22016
+    }
+  },
+  {
+    "name": "Viet Nam",
+    "min": [
+      102.14074700000009,
+      8.558609000000047
+    ],
+    "max": [
+      109.46637699999997,
+      23.33472100000006
+    ],
+    "area": 32549,
+    "lat": 21.491,
+    "lon": 105.314,
+    "population": {
+      "2005": 85028643
+    }
+  },
+  {
+    "name": "United States Virgin Islands",
+    "min": [
+      -65.026947,
+      17.676666000000125
+    ],
+    "max": [
+      -64.56028700000002,
+      18.37777699999998
+    ],
+    "area": 35,
+    "lat": 17.741,
+    "lon": -64.785,
+    "population": {
+      "2005": 111408
+    }
+  },
+  {
+    "name": "Namibia",
+    "min": [
+      11.716389,
+      -28.962502
+    ],
+    "max": [
+      25.264431,
+      -16.952778
+    ],
+    "area": 82329,
+    "lat": -22.133,
+    "lon": 17.218,
+    "population": {
+      "2005": 2019677
+    }
+  },
+  {
+    "name": "Wallis and Futuna Islands",
+    "min": [
+      -178.19110099999997,
+      -14.323891000000003
+    ],
+    "max": [
+      -176.12109399999997,
+      -13.213614000000007
+    ],
+    "area": 14,
+    "lat": -14.289,
+    "lon": -178.131,
+    "population": {
+      "2005": 15079
+    }
+  },
+  {
+    "name": "Samoa",
+    "min": [
+      -172.78060899999994,
+      -14.057502999999997
+    ],
+    "max": [
+      -171.42864999999998,
+      -13.46055599999994
+    ],
+    "area": 283,
+    "lat": -13.652,
+    "lon": -172.414,
+    "population": {
+      "2005": 183845
+    }
+  },
+  {
+    "name": "Swaziland",
+    "min": [
+      30.798332,
+      -27.316669
+    ],
+    "max": [
+      32.1334,
+      -25.728336
+    ],
+    "area": 1720,
+    "lat": -26.562,
+    "lon": 31.497,
+    "population": {
+      "2005": 1124529
+    }
+  },
+  {
+    "name": "Yemen",
+    "min": [
+      42.55583200000012,
+      12.106109999999944
+    ],
+    "max": [
+      54.47694400000006,
+      18.99934400000012
+    ],
+    "area": 52797,
+    "lat": 15.807,
+    "lon": 48.355,
+    "population": {
+      "2005": 21095679
+    }
+  },
+  {
+    "name": "Zambia",
+    "min": [
+      21.996387,
+      -18.076126
+    ],
+    "max": [
+      33.702278,
+      -8.191668
+    ],
+    "area": 74339,
+    "lat": -14.614,
+    "lon": 26.32,
+    "population": {
+      "2005": 11478317
+    }
+  },
+  {
+    "name": "Zimbabwe",
+    "min": [
+      25.236664,
+      -22.414764
+    ],
+    "max": [
+      33.073051,
+      -15.616112
+    ],
+    "area": 38685,
+    "lat": -19,
+    "lon": 29.872,
+    "population": {
+      "2005": 13119679
+    }
+  },
+  {
+    "name": "Indonesia",
+    "min": [
+      95.00802600000009,
+      -10.930000000000007
+    ],
+    "max": [
+      141.007019,
+      5.913887999999986
+    ],
+    "area": 181157,
+    "lat": -0.976,
+    "lon": 114.252,
+    "population": {
+      "2005": 226063044
+    }
+  },
+  {
+    "name": "Guadeloupe",
+    "min": [
+      -62.87306199999995,
+      15.869999000000007
+    ],
+    "max": [
+      -60.98861699999992,
+      17.93027500000005
+    ],
+    "area": 169,
+    "lat": 16.286,
+    "lon": -61.441,
+    "population": {
+      "2005": 438403
+    }
+  },
+  {
+    "name": "Netherlands Antilles",
+    "min": [
+      -69.16361999999992,
+      12.020555000000058
+    ],
+    "max": [
+      -62.93639400000001,
+      17.521942000000024
+    ],
+    "area": 80,
+    "lat": 12.123,
+    "lon": -68.87,
+    "population": {
+      "2005": 186392
+    }
+  },
+  {
+    "name": "United Arab Emirates",
+    "min": [
+      51.583327999999995,
+      22.63332900000006
+    ],
+    "max": [
+      56.38166000000001,
+      26.084159999999997
+    ],
+    "area": 8360,
+    "lat": 23.549,
+    "lon": 54.163,
+    "population": {
+      "2005": 4104291
+    }
+  },
+  {
+    "name": "Timor-Leste",
+    "min": [
+      124.0461611661691,
+      -9.463379556533425
+    ],
+    "max": [
+      127.30859400000008,
+      -8.324443999999971
+    ],
+    "area": 1487,
+    "lat": -8.822,
+    "lon": 125.878,
+    "population": {
+      "2005": 1067285
+    }
+  },
+  {
+    "name": "Pitcairn Islands",
+    "min": [
+      -130.107483,
+      -25.082225999999935
+    ],
+    "max": [
+      -124.77113300000002,
+      -24.32500499999992
+    ],
+    "area": 0,
+    "lat": -24.366,
+    "lon": -128.316,
+    "population": {
+      "2005": 5
+    }
+  },
+  {
+    "name": "Palau",
+    "min": [
+      132.2083130000001,
+      5.292220999999927
+    ],
+    "max": [
+      134.65887499999997,
+      7.729444000000058
+    ],
+    "area": 0,
+    "lat": 7.501,
+    "lon": 134.57,
+    "population": {
+      "2005": 20127
+    }
+  },
+  {
+    "name": "Marshall Islands",
+    "min": [
+      162.3235780000001,
+      5.6002770000000055
+    ],
+    "max": [
+      172.0905150000001,
+      14.598330999999916
+    ],
+    "area": 0,
+    "lat": 7.595,
+    "lon": 168.963,
+    "population": {
+      "2005": 5672
+    }
+  },
+  {
+    "name": "Saint Pierre and Miquelon",
+    "min": [
+      -56.398056,
+      46.74721499999998
+    ],
+    "max": [
+      -56.14416499999999,
+      47.13665800000007
+    ],
+    "area": 0,
+    "lat": 47.042,
+    "lon": -56.325,
+    "population": {
+      "2005": 6346
+    }
+  },
+  {
+    "name": "Saint Helena",
+    "min": [
+      -14.416112999999939,
+      -40.403892999999925
+    ],
+    "max": [
+      -5.645277999999962,
+      -7.8830560000000105
+    ],
+    "area": 0,
+    "lat": -15.953,
+    "lon": -5.71,
+    "population": {
+      "2005": 6399
+    }
+  },
+  {
+    "name": "San Marino",
+    "min": [
+      12.403889,
+      43.895554
+    ],
+    "max": [
+      12.511665,
+      43.989166
+    ],
+    "area": 0,
+    "lat": 43.942,
+    "lon": 12.46,
+    "population": {
+      "2005": 30214
+    }
+  },
+  {
+    "name": "Turks and Caicos Islands",
+    "min": [
+      -72.46806299999997,
+      21.43027500000011
+    ],
+    "max": [
+      -71.12779199999994,
+      21.957775000000026
+    ],
+    "area": 0,
+    "lat": 21.902,
+    "lon": -71.95,
+    "population": {
+      "2005": 24459
+    }
+  },
+  {
+    "name": "Western Sahara",
+    "min": [
+      -17.105278,
+      20.764095
+    ],
+    "max": [
+      -8.666389,
+      27.666958
+    ],
+    "area": 0,
+    "lat": 24.554,
+    "lon": -13.706,
+    "population": {
+      "2005": 440428
+    }
+  },
+  {
+    "name": "Serbia",
+    "min": [
+      18.81702,
+      41.855827
+    ],
+    "max": [
+      23.004997,
+      46.181389
+    ],
+    "area": 0,
+    "lat": 44.032,
+    "lon": 20.806,
+    "population": {
+      "2005": 9863026
+    }
+  },
+  {
+    "name": "Holy See (Vatican City)",
+    "min": [
+      12.445090330888604,
+      41.90142602469916
+    ],
+    "max": [
+      12.456660170953796,
+      41.90798903339123
+    ],
+    "area": 0,
+    "lat": 41.904,
+    "lon": 12.451,
+    "population": {
+      "2005": 783
+    }
+  },
+  {
+    "name": "Svalbard",
+    "min": [
+      -9.120057999999972,
+      70.80386400000009
+    ],
+    "max": [
+      36.853324999999984,
+      80.76416000000012
+    ],
+    "area": 0,
+    "lat": 78.83,
+    "lon": 18.374,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Saint Martin",
+    "min": [
+      -63.14666699999998,
+      18.05860100000001
+    ],
+    "max": [
+      -63.006393,
+      18.121944000000042
+    ],
+    "area": 0,
+    "lat": 18.094,
+    "lon": -63.041,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Saint Barthelemy",
+    "min": [
+      -63.139838999999995,
+      18.015552999999954
+    ],
+    "max": [
+      -63.01028400000001,
+      18.07036599999998
+    ],
+    "area": 0,
+    "lat": 18.04,
+    "lon": -63.043,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Guernsey",
+    "min": [
+      -2.670277999999996,
+      49.42249300000009
+    ],
+    "max": [
+      -2.5002779999999234,
+      49.50888800000001
+    ],
+    "area": 0,
+    "lat": 49.459,
+    "lon": -2.576,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Jersey",
+    "min": [
+      -2.2475000000000023,
+      49.16777000000013
+    ],
+    "max": [
+      -2.0149999999999864,
+      49.26110800000009
+    ],
+    "area": 0,
+    "lat": 49.219,
+    "lon": -2.129,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "South Georgia South Sandwich Islands",
+    "min": [
+      -38.03305799999998,
+      -59.47306099999997
+    ],
+    "max": [
+      -26.241390000000024,
+      -53.989723000000026
+    ],
+    "area": 0,
+    "lat": -54.209,
+    "lon": -36.891,
+    "population": {
+      "2005": 0
+    }
+  },
+  {
+    "name": "Taiwan",
+    "min": [
+      118.20583299999998,
+      21.927773000000116
+    ],
+    "max": [
+      122.00221299999998,
+      26.229717000000107
+    ],
+    "area": 0,
+    "lat": 23.754,
+    "lon": 120.946,
+    "population": {
+      "2005": 0
+    }
+  }
+]

BIN
threejs/resources/data/world/country-outlines-4k.png


+ 214 - 0
threejs/threejs-align-html-elements-to-3d-globe-too-many-labels.html

@@ -0,0 +1,214 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Align HTML Elements to 3D Globe</title>
+    <style>
+    body {
+        margin: 0;
+        font-family: sans-serif;
+    }
+    #c {
+        width: 100%;  /* let our container decide our size */
+        height: 100%;
+        display: block;
+    }
+    #container {
+      position: relative;  /* makes this the origin of its children */
+      width: 100vw;
+      height: 100vh;
+      overflow: hidden;
+    }
+    #labels {
+      position: absolute;  /* let us position ourself inside the container */
+      left: 0;             /* make our position the top left of the container */
+      top: 0;
+      color: white;
+    }
+    #labels>div {
+      position: absolute;  /* let us position them inside the container */
+      left: 0;             /* make their default position the top left of the container */
+      top: 0;
+      cursor: pointer;     /* change the cursor to a hand when over us */
+      font-size: small;
+      user-select: none;   /* don't let the text get selected */
+      pointer-events: none;  /* make us invisible to the pointer */
+      text-shadow:         /* create a black outline */
+        -1px -1px 0 #000,
+         0   -1px 0 #000,
+         1px -1px 0 #000,
+         1px  0   0 #000,
+         1px  1px 0 #000,
+         0    1px 0 #000,
+        -1px  1px 0 #000,
+        -1px  0   0 #000;
+    }
+    #labels>div:hover {
+      color: red;
+    }
+    </style>
+  </head>
+  <body>
+    <div id="container">
+      <canvas id="c"></canvas>
+      <div id="labels"></div>
+    </div>
+  </body>
+<script src="resources/threejs/r102/three.js"></script>
+<script src="resources/threejs/r102/js/utils/BufferGeometryUtils.js"></script>
+<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 60;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2.5;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.rotateSpeed = 0.1;
+  controls.enablePan = false;
+  controls.minDistance = 1.2;
+  controls.maxDistance = 4;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('#236');
+
+  {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
+    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+    const material = new THREE.MeshBasicMaterial({map: texture});
+    scene.add(new THREE.Mesh(geometry, material));
+  }
+
+  async function loadJSON(url) {
+    const req = await fetch(url);
+    return req.json();
+  }
+
+  let countryInfos;
+  async function loadCountryData() {
+    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejsfundamentals: url */
+
+    const lonFudge = Math.PI * 1.5;
+    const latFudge = Math.PI;
+    // these helpers will make it easy to position the boxes
+    // We can rotate the lon helper on its Y axis to the longitude
+    const lonHelper = new THREE.Object3D();
+    // We rotate the latHelper on its X axis to the latitude
+    const latHelper = new THREE.Object3D();
+    lonHelper.add(latHelper);
+    // The position helper moves the object to the edge of the sphere
+    const positionHelper = new THREE.Object3D();
+    positionHelper.position.z = 1;
+    latHelper.add(positionHelper);
+
+    const labelParentElem = document.querySelector('#labels');
+    for (const countryInfo of countryInfos) {
+      const {lat, lon, name} = countryInfo;
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.Math.degToRad(lon) + lonFudge;
+      latHelper.rotation.x = THREE.Math.degToRad(lat) + latFudge;
+
+      // get the position of the lat/lon
+      positionHelper.updateWorldMatrix(true, false);
+      const position = new THREE.Vector3();
+      positionHelper.getWorldPosition(position);
+      countryInfo.position = position;
+
+      // add an element for each country
+      const elem = document.createElement('div');
+      elem.textContent = name;
+      labelParentElem.appendChild(elem);
+      countryInfo.elem = elem;
+    }
+    requestRenderIfNotRequested();
+  }
+  loadCountryData();
+
+  const tempV = new THREE.Vector3();
+
+  function updateLabels() {
+    // exit if we have not yet loaded the JSON file
+    if (!countryInfos) {
+      return;
+    }
+
+    for (const countryInfo of countryInfos) {
+      const {position, elem} = countryInfo;
+
+      // get the normalized screen coordinate of that position
+      // x and y will be in the -1 to +1 range with x = -1 being
+      // on the left and y = -1 being on the bottom
+      tempV.copy(position);
+      tempV.project(camera);
+
+      // convert the normalized position to CSS coordinates
+      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+      // move the elem to that position
+      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+    }
+  }
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let renderRequested = false;
+
+  function render() {
+    renderRequested = undefined;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+
+    updateLabels();
+
+    renderer.render(scene, camera);
+  }
+  render();
+
+  function requestRenderIfNotRequested() {
+    if (!renderRequested) {
+      renderRequested = true;
+      requestAnimationFrame(render);
+    }
+  }
+
+  controls.addEventListener('change', requestRenderIfNotRequested);
+  window.addEventListener('resize', requestRenderIfNotRequested);
+}
+
+main();
+</script>
+</html>
+

+ 256 - 0
threejs/threejs-align-html-elements-to-3d-globe.html

@@ -0,0 +1,256 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Align HTML Elements to 3D Globe</title>
+    <style>
+    body {
+        margin: 0;
+        font-family: sans-serif;
+    }
+    #c {
+        width: 100%;  /* let our container decide our size */
+        height: 100%;
+        display: block;
+    }
+    #container {
+      position: relative;  /* makes this the origin of its children */
+      width: 100vw;
+      height: 100vh;
+      overflow: hidden;
+    }
+    #labels {
+      position: absolute;  /* let us position ourself inside the container */
+      left: 0;             /* make our position the top left of the container */
+      top: 0;
+      color: white;
+    }
+    #labels>div {
+      position: absolute;  /* let us position them inside the container */
+      left: 0;             /* make their default position the top left of the container */
+      top: 0;
+      cursor: pointer;     /* change the cursor to a hand when over us */
+      font-size: small;
+      user-select: none;   /* don't let the text get selected */
+      pointer-events: none;  /* make us invisible to the pointer */
+      text-shadow:         /* create a black outline */
+        -1px -1px 0 #000,
+         0   -1px 0 #000,
+         1px -1px 0 #000,
+         1px  0   0 #000,
+         1px  1px 0 #000,
+         0    1px 0 #000,
+        -1px  1px 0 #000,
+        -1px  0   0 #000;
+    }
+    #labels>div:hover {
+      color: red;
+    }
+    </style>
+  </head>
+  <body>
+    <div id="container">
+      <canvas id="c"></canvas>
+      <div id="labels"></div>
+    </div>
+  </body>
+<script src="resources/threejs/r102/three.js"></script>
+<script src="resources/threejs/r102/js/utils/BufferGeometryUtils.js"></script>
+<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+<script src="../3rdparty/dat.gui.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE, dat */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 60;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2.5;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.rotateSpeed = 0.1;
+  controls.enablePan = false;
+  controls.minDistance = 1.2;
+  controls.maxDistance = 4;
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('#246');
+
+  {
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
+    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);
+    const material = new THREE.MeshBasicMaterial({map: texture});
+    scene.add(new THREE.Mesh(geometry, material));
+  }
+
+  async function loadJSON(url) {
+    const req = await fetch(url);
+    return req.json();
+  }
+
+  let countryInfos;
+  async function loadCountryData() {
+    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejsfundamentals: url */
+
+    const lonFudge = Math.PI * 1.5;
+    const latFudge = Math.PI;
+    // these helpers will make it easy to position the boxes
+    // We can rotate the lon helper on its Y axis to the longitude
+    const lonHelper = new THREE.Object3D();
+    // We rotate the latHelper on its X axis to the latitude
+    const latHelper = new THREE.Object3D();
+    lonHelper.add(latHelper);
+    // The position helper moves the object to the edge of the sphere
+    const positionHelper = new THREE.Object3D();
+    positionHelper.position.z = 1;
+    latHelper.add(positionHelper);
+
+    const labelParentElem = document.querySelector('#labels');
+    for (const countryInfo of countryInfos) {
+      const {lat, lon, min, max, name} = countryInfo;
+
+      // adjust the helpers to point to the latitude and longitude
+      lonHelper.rotation.y = THREE.Math.degToRad(lon) + lonFudge;
+      latHelper.rotation.x = THREE.Math.degToRad(lat) + latFudge;
+
+      // get the position of the lat/lon
+      positionHelper.updateWorldMatrix(true, false);
+      const position = new THREE.Vector3();
+      positionHelper.getWorldPosition(position);
+      countryInfo.position = position;
+
+      // compute the area for each country
+      const width = max[0] - min[0];
+      const height = max[1] - min[1];
+      const area = width * height;
+      countryInfo.area = area;
+
+      // add an element for each country
+      const elem = document.createElement('div');
+      elem.textContent = name;
+      labelParentElem.appendChild(elem);
+      countryInfo.elem = elem;
+    }
+    requestRenderIfNotRequested();
+  }
+  loadCountryData();
+
+  const tempV = new THREE.Vector3();
+  const normalMatrix = new THREE.Matrix3();
+  const positiveZ = new THREE.Vector3(0, 0, 1);
+
+  const settings = {
+    minArea: 20,
+    visibleAngleDeg: 75,
+  };
+  const gui = new dat.GUI({width: 300});
+  gui.add(settings, 'minArea', 0, 50).onChange(requestRenderIfNotRequested);
+  gui.add(settings, 'visibleAngleDeg', 0, 180).onChange(requestRenderIfNotRequested);
+
+  function updateLabels() {
+    if (!countryInfos) {
+      return;
+    }
+
+    const large = settings.minArea * settings.minArea;
+    const visibleDot = Math.cos(THREE.Math.degToRad(settings.visibleAngleDeg));
+    // get a matrix that represents a relative orientation of the camera
+    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
+    for (const countryInfo of countryInfos) {
+      const {position, elem, area} = countryInfo;
+      // large enough?
+      if (area < large) {
+        elem.style.display = 'none';
+        continue;
+      }
+
+      // orient the position based on the camera's orientation
+      tempV.copy(position);
+      tempV.applyMatrix3(normalMatrix);
+
+      // get the dot product with positiveZ
+      // -1 = facing directly away and +1 = facing directly toward us
+      const dot = tempV.dot(positiveZ);
+
+      // if the orientation is not facing us hide it.
+      if (dot < visibleDot) {
+        elem.style.display = 'none';
+        continue;
+      }
+
+      // restore the element to its default display style
+      elem.style.display = '';
+
+      // get the normalized screen coordinate of that position
+      // x and y will be in the -1 to +1 range with x = -1 being
+      // on the left and y = -1 being on the bottom
+      tempV.copy(position);
+      tempV.project(camera);
+
+      // convert the normalized position to CSS coordinates
+      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+      // move the elem to that position
+      countryInfo.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+    }
+  }
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let renderRequested = false;
+
+  function render() {
+    renderRequested = undefined;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+
+    updateLabels();
+
+    renderer.render(scene, camera);
+  }
+  render();
+
+  function requestRenderIfNotRequested() {
+    if (!renderRequested) {
+      renderRequested = true;
+      requestAnimationFrame(render);
+    }
+  }
+
+  controls.addEventListener('change', requestRenderIfNotRequested);
+  window.addEventListener('resize', requestRenderIfNotRequested);
+}
+
+main();
+</script>
+</html>
+

+ 190 - 0
threejs/threejs-align-html-to-3d-w-hiding.html

@@ -0,0 +1,190 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Align HTML Elements w/hiding</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100%;  /* let our container decide our size */
+        height: 100%;
+        display: block;
+    }
+    #container {
+      position: relative;  /* makes this the origin of its children */
+      width: 100vw;
+      height: 100vh;
+      overflow: hidden;
+    }
+    #labels {
+      position: absolute;  /* let us position ourself inside the container */
+      left: 0;             /* make our position the top left of the container */
+      top: 0;
+      color: white;
+    }
+    #labels>div {
+      position: absolute;  /* let us position them inside the container */
+      left: 0;             /* make their default position the top left of the container */
+      top: 0;
+      cursor: pointer;     /* change the cursor to a hand when over us */
+      font-size: large;
+      user-select: none;   /* don't let the text get selected */
+      text-shadow:         /* create a black outline */
+        -1px -1px 0 #000,
+         0   -1px 0 #000,
+         1px -1px 0 #000,
+         1px  0   0 #000,
+         1px  1px 0 #000,
+         0    1px 0 #000,
+        -1px  1px 0 #000,
+        -1px  0   0 #000;
+    }
+    #labels>div:hover {
+      color: red;
+    }
+    </style>
+  </head>
+  <body>
+    <div id="container">
+      <canvas id="c"></canvas>
+      <div id="labels"></div>
+    </div>
+  </body>
+<script src="resources/threejs/r102/three.min.js"></script>
+<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 1.1;
+  const far = 20;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 7;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const labelContainerElem = document.querySelector('#labels');
+
+  function makeInstance(geometry, color, x, name) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    const elem = document.createElement('div');
+    elem.textContent = name;
+    labelContainerElem.appendChild(elem);
+
+    return {cube, elem};
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0, 'Aqua'),
+    makeInstance(geometry, 0x8844aa, -2, 'Purple'),
+    makeInstance(geometry, 0xaa8844,  2, 'Gold'),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const tempV = new THREE.Vector3();
+  const raycaster = new THREE.Raycaster();
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cubeInfo, ndx) => {
+      const {cube, elem} = cubeInfo;
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+
+      // get the position of the center of the cube
+      cube.updateWorldMatrix(true, false);
+      cube.getWorldPosition(tempV);
+
+      // get the normalized screen coordinate of that position
+      // x and y will be in the -1 to +1 range with x = -1 being
+      // on the left and y = -1 being on the bottom
+      tempV.project(camera);
+
+      // ask the raycaster for all the objects that intersect
+      // from the eye toward this object's position
+      raycaster.setFromCamera(tempV, camera);
+      const intersectedObjects = raycaster.intersectObjects(scene.children);
+      // We're visible if the first intersection is this object.
+      const show = intersectedObjects.length && cube === intersectedObjects[0].object;
+
+      if (!show || Math.abs(tempV.z) > 1) {
+        // hide the label
+        elem.style.display = 'none';
+      } else {
+        // unhide the label
+        elem.style.display = '';
+
+        // convert the normalized position to CSS coordinates
+        const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+        const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+        // move the elem to that position
+        elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+      }
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 174 - 0
threejs/threejs-align-html-to-3d.html

@@ -0,0 +1,174 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Align HTML Elements to 3D</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100%;  /* let our container decide our size */
+        height: 100%;
+        display: block;
+    }
+    #container {
+      position: relative;  /* makes this the origin of its children */
+      width: 100vw;
+      height: 100vh;
+      overflow: hidden;
+    }
+    #labels {
+      position: absolute;  /* let us position ourself inside the container */
+      left: 0;             /* make our position the top left of the container */
+      top: 0;
+      color: white;
+    }
+    #labels>div {
+      position: absolute;  /* let us position them inside the container */
+      left: 0;             /* make their default position the top left of the container */
+      top: 0;
+      cursor: pointer;     /* change the cursor to a hand when over us */
+      font-size: large;
+      user-select: none;   /* don't let the text get selected */
+      text-shadow:         /* create a black outline */
+        -1px -1px 0 #000,
+         0   -1px 0 #000,
+         1px -1px 0 #000,
+         1px  0   0 #000,
+         1px  1px 0 #000,
+         0    1px 0 #000,
+        -1px  1px 0 #000,
+        -1px  0   0 #000;
+    }
+    #labels>div:hover {
+      color: red;
+    }
+    </style>
+  </head>
+  <body>
+    <div id="container">
+      <canvas id="c"></canvas>
+      <div id="labels"></div>
+    </div>
+  </body>
+<script src="resources/threejs/r102/three.min.js"></script>
+<script src="resources/threejs/r102/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 1.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 7;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const labelContainerElem = document.querySelector('#labels');
+
+  function makeInstance(geometry, color, x, name) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    const elem = document.createElement('div');
+    elem.textContent = name;
+    labelContainerElem.appendChild(elem);
+
+    return {cube, elem};
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0, 'Aqua'),
+    makeInstance(geometry, 0x8844aa, -2, 'Purple'),
+    makeInstance(geometry, 0xaa8844,  2, 'Gold'),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const tempV = new THREE.Vector3();
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cubeInfo, ndx) => {
+      const {cube, elem} = cubeInfo;
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+
+      // get the position of the center of the cube
+      cube.updateWorldMatrix(true, false);
+      cube.getWorldPosition(tempV);
+
+      // get the normalized screen coordinate of that position
+      // x and y will be in the -1 to +1 range with x = -1 being
+      // on the left and y = -1 being on the bottom
+      tempV.project(camera);
+
+      // convert the normalized position to CSS coordinates
+      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
+      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
+
+      // move the elem to that position
+      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+