How to write your own projection for Globus?

Hi,

I’ve been bragging about Globus recently – allegedly, you can make your own projections for it. It’s true, let me show you how to do it!

First here are links to some projections, which are available from the drop down list. We’ll use them for reference:

Please open equirectangular. First thing you’ll see is:

...SECTION TO-CALCULATIONS:

Projection files are divided into sections. There are 7 sections:

  1. TO-CALCULATIONS – responsible for calculations if you convert TO this projection.
  2. FROM-CALCULATIONS – responsible for calculations if you convert FROM this projection.
  3. TO-FORM – if you convert to this projection, this is the HTML form that appears below Project to:. You will use it to get additional data from user.
  4. FROM-FORM: if you convert from this projection, this is the HTML form that appears below Project from:. You will use it to get additional data about the image you’re working with – what is its central meridian, etc.
  5. OUTPUT-WIDTH-CALCULATIONS – javascript code which is responsible for setting width of output image.
  6. OUTPUT-HEIGHT-CALCULATIONS – javascript code which is responsible for setting height of output image.
  7. MERIDIAN-LENGTH-CALCULATIONS – javascript code which will calculate length of meridian in pixels based on width and height of input image.

Now let me tell you what exactly does each section, and how to write them. Let’s project from Equirectangular to Sinusoidal. But beware, I will not go in order!

3. TO-FORM:

When we project to Sinusoidal, we want the user to pick the meridian that will be in center. What is the way to collect data from a user on a website? HTML forms. We need to write html code that will appear below Project to:, and will let the user pick the central meridian. Here’s how we did it:

1
Pick central meridian (from -180 to 180): <input max=“180” min=“-180” name=“meridian” type=“number” />

Fairly simple form – just one input. You can write any html code you wish, you may even add <script> … </script> sections. Then, once the user presses Project, Globus Map Projector will extract the data from the form, and enable you to use it when calculating the projection. However, only some input types will be extracted by Globus:

  • <input type=”number” name=”someFloat”>
  • <input type=”color” name=”someColor”>
  • <input type=”range” name=”someFloat>
  • <input type=”checkbox” name=”someBool”>
  • <input type=”radio” name=”someInteger”>
  • <select name=”someInteger”>

As you have probably noticed each of those inputs was also given a name. It is crucial – you will use this name as a name of a variable when you calculate the projection. You will learn how to use those variables in the next section.

1. TO-CALCULATIONS:

This is the very heart of our projection. Here we define how to project TO our sinusoidal projection. Such calculations are done on shaders in Globus Map Projector, so this section must be written in GLSL (GL Shader Language). Basically, it is very similar to C, however it has some important differences, which I will list at the end of this section.

TO-CALCULATIONS’s job is simple. As input you get coordinates of a point in your TO projection. Then you need to return where this point is located in equirectangular projection. Let me show this in a picture:

We get a point on output image, for example point (x, y):

sinusoidalWithPoint

 

We need to find where this point is in equirectangular projection. When TO-CALCULATIONS’s section finishes, color from pixel (x2, y2) will be set in our output image at pixel (x, y).

equirectWithPoint

 

Let’s go to practice. TO-CALCULATIONS section is body (code between the curly brackets) of a function of this type:

1
2
3
4
5
6
7
8
vec2 projectPointFromSinusoidalToEquirectangular(vec2 coords)
{
    // do your projection code
    float x = coords.x; // coords.x is X coordinate of projected point (check the black and white image few lines earlier)
    float y = coords.y; // coords.y is Y coordinate of projected point

    return vec2(x, y); // we return a vector which contains point (x2, y2) from above image.
}

As you have probably noticed, this code does nothing – it projects a point to itself. To make a real projection we need to learn three more things.

1. All coordinates (input and returned) are floating point numbers between 0 and 1. x = 0 is the left side, x = 1 is the right side, y = 0 is the top, and  y = 1 is the bottom.

2. If we want a given point to be blank (if the point doesn’t belong to the projection), we return vector with at least one negative coordinate. For example: return vec2(-1.0, -1.0); // this point is blank

3. We get access to our variables from TO-FORM sectionWe will call the variables that the user created uniforms (because they behave just like GLSL’s uniforms), and they are constants defined at the top of our function. Check this list:

  • <input type=”number” name=”someFloat”> will generate:
    1
    const float someFloat = …..; // number provided by the user
  • <input type=”color” name=”someColor”> will generate:
    1
    const vec4 someColor = …..; // color provided by the user
  • <input type=”range” name=”someFloat”> will generate:
    1
    2
    const float someFloat = …..; // selected number from range. For example if the user picked 12/250
    //  this will be: const float someFloat = 12.0;
  • <input type=”checkbox” name=”someBool”> will generate:
    1
    const bool someBool = …..; // true if the checkbox was selected, false if it wasn’t selected
  • <input type=”radio” name=”someInt”> will generate:
    1
    2
    3
    4
    5
    const int someInt = …..; // Globus will count all the radios with the same name and return index
                               // of the one that was selected. For example, if we have 5 radios with
                               // name “radio123”, in 5 lines, and the radio in 3rd line was selected
                               // we’ll get: <em>const int radio123 = 2;</em> and if the radio from 1st line was
                               // selected we’ll get: <em>const int radio 123 = 0;</em>
  • <select name=”someInt”> will generate:
    1
    const int someInt = …..; // index of selected option (remember we count indexes starting from 0)

Knowing that we can now fully understand our sinusoidal projection. Analyze it yourself, it should be quite simple now!

Important things to know about GLSL in WebGL:

  1. When you make any type of operations (conditions, mathematical operations, etc.) on numbers, they need to be of the same type. Say you want to check if 5 < 6.12. It will give you a compiler error, as it should be 5.0 < 6.12. Or you have float number and int i. In this case you need to do it like: number + float(i). Which leads us to:
  2. Conversion. Unlike in C++, or C you don’t have the typical (int) myFloat construct. You will use constructors – just like this: int(myFloat) or float(myInt).
  3. In WebGL version of GLSL you cannot use loops. Well, most of the times. While won’t work, same with do…whileFor works only if the condition is evaluable during compilation. What it means, is that your for usually has to be of this type: for (int i = 0; i < someConstant; ++i). Lucky for you, all the numbers provided by the user in HTML forms are constant.

4. FROM-FORM:

Very similar to TO-FORM. However, in this form, the user writes with what parameters they created the FROM projection. In our case, if the FROM projection (equirectangular) was made using 80 as central meridian, the user writes 80 in pick central meridian. Data collected from user in this section will be sent to FROM-CALCULATIONS.

2. FROM-CALCULATIONS:

Opposite to TO-CALCULATIONS – you need to calculate, where a point in equirectangular projection is on your projection. Let me illustrate that.

You have a point (x2, y2) on equirectangular projection:

equirectWithPoint

 

And then, you want to find its position on your projection (or, in other words, project it to your projection):

sinusoidalWithPoint

FROM-CALCULATIONS programming is identical to TO-CALCULATIONS. You need to write body (code between the curly brackets) of a function of this type:

1
2
3
4
5
6
7
8
vec2 projectPointFromEquirectangularToSinusoidal(vec2 coords)
{
    // do your projection code
    float x = coords.x; // coords.x is x2 from pictures above
    float y = coords.y; // coords.y is y2 from pictures above

    return vec2(x, y); // we return a vector which contains point (x, y) from above image.
}

You will have access to uniforms from FROM-FORM just as in TO-CALCULATIONS you get access to uniforms from TO-FORM.

7. MERIDIAN-LENGTH-CALCULATIONS:

This section’s job is to say how long (in pixels) a meridian is in our FROM projection. In our case, the input image is in equirectangular (we project from equirectangular), so meridans span from the very top of the image to the very bottom:

Equirectangular-projection

Equirectangular map from NASA – public domain

That’s why the length of meridian in pixels is input image height. However, we also know that in equirectangular projections height = width/2, so meridian = width/2. Of course we could use meridian = height just as well.

Ok, so now how to write it?

MERIDIAN-LENGTH-CALCULATIONS section is javascript code. Imagine it is a function (it actualy is a function, run through eval()) which gets width and height of the input image, and uniforms (from Project from form). Your job is to write body of a function (code between curly brackets) that will return the length of meridian.

1
2
3
4
5
function calculateMeridianLength(width, height, uniforms)
{
    // do some code
    return …..; // return length of meridian
}

Few lines ago I mentioned that MERIDIAN-LENGTH-CALCULATIONS also receives the uniforms from FROM-FORM. Why? Check sinusoidal intercepted projection – it turns out, that we can make the gores either parallel or concentric. In each of those cases projection size is different: in parallels width/height = 2, but in concentric width = height. This means that we have to distinguish type of gores when we calculate length of meridian. That’s when uniforms come in handy. Uniforms is an array of uniforms, and each uniform is a structure containing 3 fields:

  • type (float, int, bool, vec4)
  • name (taken directly from HTML form)
  • value (value given by the user)

If we want to get a uniform of a given name from this array, we can use getUniformFromArray(array, name) method. It’s quite easy, let’s check it on sinusoidal intercepted projection:

1
2
3
4
5
6
7
8
9
switch (getUniformFromArray(uniforms, ‘interceptionType’).value)
{
    case 0:
        return width/2;
        break;
    case 1:
        return width;
        break;
}

Let’s concentrate on:

getUniformFromArray(uniforms, 'interceptionType').value

As you can see, we simply ask to extract uniform of name interceptionType, and once we get it, we extract its value. Quite simple, isn’t it?

5, 6. OUTPUT-WIDTH-CALCULATIONS and OUTPUT-HEIGHT-CALCULATIONS:

This javascript sections are used to set the size of output image. The input is length of meridian in pixels and uniforms from TO-FORM; as the output, you need to return either width or height.

 

I hope that now it’s clear how to make your own projections. I encourage you to write some, and you can send them to winski@winski.net, so I can check them, and then add them to the website. Don’t forget to write your name (in the code or in the forms) if you want attribution, and give permission to use it or add license notice to the code.

Write in comments, if you have questions!

Thank you and cheers 🙂

Posted in Map projections.
  • Vorropohaiah

    Hi, first of all I’d like to thank you for taking to the time to publish this and allow others to use it!

    I’m trying to convert an equirectangular map into a sinusoidal interrupted projection (with 10 or 20 degree gores) with the intention of making a globe with the gores once printed out. However, I’m running into a problem with the lines of latitude – sinusoidal projection projects the latitudes as parallels, however this results in lines of latitude that are geometric rather than circular, particularly close to the poles (I hope I’m making sense).

    Is there another projection, similar to sinusoidal interrupted that distorts the lines of latitude in such a way that when printed and attached to a globe would result in circular lines of latitude, as desired? Either way, is it possible to use the code above to make such a projection?

    thanks for your time!

    • winski

      Hi, in this situation, your best bet is to either increase the number of gores, or use the daisy-petal projection. It is pretty hard to find a projector for daisy-petal on the internet, and my Globus Map Projector’s daisy-petal seems to have artifacts. However, if you are on Windows, you can use my USGS daisy-petal creator from the programs section (http://www.winski.net/?page_id=7).

      • http://kai.dyndns.org/ Kai Carver

        Hi, nice page, but your USGS daisy-petal creator at

        http://www.winski.net/wp-content/uploads/2015/12/goresx64.zip

        produces gore maps, not daisy-petal maps.

        • winski

          Hi,
          Thank you for telling me that. I am currently away from home, and I don’t have the code for this creator here. However, I’ll try to rewrite it ASAP, which might be after Christmas.
          Happy Holidays

        • winski

          Hi,
          I fixed the link. x32 and x64 versions are now available and should work properly. Tell me if you run into any problems.

          • http://kai.dyndns.org/ Kai Carver

            Thank you for fixing that! It’s almost perfect, but there is still a slight artifact, see attached image: a thin horizontal white line through the middle of the image. which disappears at the poles. But it’s already quite good.