Miller Projection to pixels

2019-07-14 04:51发布

问题:

I'm trying to use Miller Projection to convert coordinates to pixels. My method looks like this:

function millerProjection(lat, lng) {

        // Create sec() function //
        function sec(value) {
            return 1/Math.cos(value);
        }

        // Create fucntion to change degree to radian //
        function toRadian(value) {
            return value * Math.PI / 180;
        }

        lng = toRadian(lng);
        lat = toRadian(lat);

        // Miller Projection
        // var x = lng;
        // var y = 1.25 * Math.log(Math.tan(Math.PI / 4 + 0.4 * (lat)));

        // Mercator Projection
        // var x  = lng;
        // var y = Math.log(Math.tan(lat) + sec(lat));

        var mapSet = {
            leftLong: toRadian(-180),
            rightLong: toRadian(180),
            topLat: toRadian(90),
            bottomLat: toRadian(-90),
            imageWidth: 2057,
            imageHeight: 1512,
        }

        var x = (lng - mapSet.leftLong) * (mapSet.imageWidth / (mapSet.rightLong - mapSet.leftLong));
        var y = (mapSet.topLat - lat) * (mapSet.imageHeight / (mapSet.topLat - mapSet.bottomLat));

        console.log(`Miller Projection X: ${x} -- Y: ${y}`);

        return { x: x,  y: y };

    }

I'm using this picture as a map: https://upload.wikimedia.org/wikipedia/commons/5/5f/Miller_projection_SW.jpg

Apparently If I use 0, 0 coordinates it marks the correct location. If I give it any other coordinates it's not working. Can the map be the problem or maybe there is an issue with the logic I use?

回答1:

This:

  var x = (lng - mapSet.leftLong) * (mapSet.imageWidth / (mapSet.rightLong - mapSet.leftLong));
  var y = (mapSet.topLat - lat) * (mapSet.imageHeight / (mapSet.topLat - mapSet.bottomLat));

Assumes a linear x and y transform for both latitude and longitude, but this only occurs for longitude. You can see the different spacings between latitudes in the image you reference.


Let's go back to your commented out projection and use that as it is correct, but needs scaling and translation:

function millerProjection(lat, lng) {

  // Create sec() function
  function sec(value) {
    return 1/Math.cos(value);
  }

 // Create fucntion to change degree to radians
 function toRadian(value) {
   return value * Math.PI / 180;
 }
 
 lng = toRadian(lng);
 lat = toRadian(lat);

 // Miller Projection
 var x = lng;
 var y = 1.25*Math.log(Math.tan(Math.PI/4+0.4*(lat)));
 
 return [x,y];
 
}

console.log(millerProjection(90,180));
console.log(millerProjection(-90,-180));

The output range for the x axis is -π to + π, and the y axis has a range about 0.733 times that.

Now we can scale and translate. I'll scale first and translate later, but vice versa is just as easy.

To scale, we need to know the width or height of the bounding box or output range. The aspect is fixed, so this is easiest if not specifying both, but rather determining one from the other. If we stretch the outputs unevenly, we no longer have a Miller.

Given a dimension for width we might scale with something like:

var scale = width / Math.PI / 2; 

We want to see how many pixels are needed for each radian. Then we can multiple the projection output by the scale to get scaled values. Using the above, we can also validate our projection using a library like d3's geoprojection module:

function millerProjection(lat, lng) {

  // Create sec() function
  function sec(value) {
    return 1/Math.cos(value);
  }

 // Create fucntion to change degree to radians
 function toRadian(value) {
   return value * Math.PI / 180;
 }
 
 lng = toRadian(lng);
 lat = toRadian(lat);

 // Miller Projection
 var x = lng;
 var y = -1.25*Math.log(Math.tan(Math.PI/4+0.4*(lat)));
 
 var width = 360;
 var scale = width/Math.PI/2;
 x *= scale;
 y *= scale;
 
 return [x,y];
 
}

//// 
// set up reference scale:
var width = 360;
var height = width / Math.PI / 2.303412543376391; // aspect ratio

// set d3 projection for reference:
var d3Miller = d3.geoMiller()
 .fitSize([360,180*0.7331989845], {"type": "Polygon","coordinates": [[[180, 90], [-180, 90], [-90, -180], [180, 90]]] })
	.translate([0,0]);
  
// compare the two:
console.log("90N,180W:")
console.log("Miller:  ", ...millerProjection(90,-180));
console.log("d3Miller:", ...d3Miller([-180,90]));

console.log("90S,180E:")
console.log("Miller:  ",...millerProjection(-90,180));
console.log("d3Miller:", ...d3Miller([180,-90]));

console.log("90S,180E:")
console.log("Miller:  ",...millerProjection(-57,162));
console.log("d3Miller:", ...d3Miller([162,-57]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-geo.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>

I've taken the negative of the latitude (based on your example) because while projected geographic coordinates have y = 0 at the bottom - with y values increasing as one moves up. Conversely, things like (most?) images have y = 0 at the top - with y values increasing as one moves down. D3 anticipates the latter convention so I did not do it for the reference function

Looks good. Our data now has a range of -width/2 to width/2 on the x axis, and again on the y, about 0.733 times that. Let's translate the data so that it occupies the bounding box of with a bottom left coordinate of [0,0] and a top right coordinate of [width,width*0.733]. This is fairly easy, after we scale the data, we add width/2 to the x value, and width/2*0.733 (or slightly more precisely, width/2/*0.7331989845) to the y value:

function millerProjection(lat, lng) {

  // Create sec() function
  function sec(value) {
    return 1/Math.cos(value);
  }

 // Create fucntion to change degree to radians
 function toRadian(value) {
   return value * Math.PI / 180;
 }
 
 lng = toRadian(lng);
 lat = toRadian(lat);

 // Miller Projection
 var x = lng;
 var y = -1.25*Math.log(Math.tan(Math.PI/4+0.4*(lat)));
 
 var width = 2057;
 var scale = width/Math.PI/2;
 x *= scale;
 y *= scale;
 
 x += width/2;
 y += width/2*0.7331989845
 
 return [x,y];
 
}
  
// compare the two:
console.log("90N,180W:")
console.log("Miller:  ", ...millerProjection(90,-180));

console.log("90S,180E:")
console.log("Miller:  ",...millerProjection(-90,180));

console.log("45N,45E:")
console.log("Miller:  ",...millerProjection(45,45));

console.log("90S,180E:")
console.log("Miller:  ",...millerProjection(-57,162));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-geo.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>

Of course, if you are doing image manipulation where the top should be y=0 and y values increase if you move down, flip the sign of the latitude before doing any manipulation.