luxcem luxcem - 5 months ago 26
Javascript Question

Positioning image on Google Maps with rotate / scale / translate

I'm developing a user-interface for positioning an image on a google map.
I started from : http://overlay-tiler.googlecode.com/svn/trunk/upload.html which is pretty close to what I want.

But instead of 3 contact points I want a rotate tool, a scale tool and a translate tool (the later exists).

I tried to add a rotate tool but it doesn't work as I expected :

I put a dot on the left bottom corner that control the rotation (around the center of the image). The mouse drag the control dot and I calculate the 3 others points.

My code is based on the mover object but I changed the onMouseMove function :

overlaytiler.Rotater.prototype.rotateDot_ = function(dot, theta, origin) {
dot.x = ((dot.x - origin.x) * Math.cos(theta) - (dot.y - origin.y) * Math.sin(theta)) + origin.x;
dot.y = ((dot.x - origin.x) * Math.sin(theta) + (dot.y - origin.y) * Math.cos(theta)) + origin.y;
dot.render();
};

overlaytiler.Rotater.prototype.onMouseMove_ = function(e) {
var dots = this.controlDots_;
var center = overlaytiler.Rotater.prototype.getCenter_(dots);

// Diagonal length
var r = Math.sqrt(Math.pow(this.x - center.x, 2) + Math.pow(this.y - center.y, 2));
var old = {
x: this.x,
y: this.y
};

// Real position
var newPos = {
x: this.x + e.clientX - this.cx,
y: this.y + e.clientY - this.cy
}

var newR = Math.sqrt(Math.pow(newPos.x - center.x, 2) + Math.pow(newPos.y - center.y, 2));
var theta = - Math.acos((2 * r * r - (Math.pow(newPos.x - old.x, 2) + Math.pow(newPos.y - old.y, 2))) / (2 * r * r));

// Fixed distance position
this.x = (newPos.x - center.x) * (r / newR) + center.x;
this.y = (newPos.y - center.y) * (r / newR) + center.y;

dots[1].x = center.x + (center.x - this.x);
dots[1].y = center.y + (center.y - this.y);
dots[1].render();

overlaytiler.Rotater.prototype.rotateDot_(dots[2], theta, center);
overlaytiler.Rotater.prototype.rotateDot_(dots[0], theta, center);

// Render
this.render();

this.cx = e.clientX;
this.cy = e.clientY;
};


Unfortunately there is a problem with precision and angle sign.

http://jsbin.com/iQEbIzo/4/

After a few rotations the image is highly distorted and rotation is supported only in one direction.

I wonder how I can achieve a great precision and without any distortion.

Maybe my approach is useless here (try to move the corners at the right coordinates), I tried to rotate the image with the canvas but my attempts were unsuccessful.

Edit : Full working version : http://jsbin.com/iQEbIzo/7/

Answer

rotation is supported only in one direction

This is due to how you calculate the angle between two vectors. It always gives you the same vector no matter if the mouse is right of the dot or not. I've found a solution in a german math board (unfortunately I cant access the site without using the cache of Google : cached version).

Two vectors

Note that in this example the angle α is on both sides the same and not as you would expect -α in the second one. To find out if the vector a is always on "the same side" of vector b you can use this formula.

ax*by - ay*bx

This is either positive or negative. You you simply can change the sign of the angle to α * -1.

I modified some parts of your code.

overlaytiler.Rotater.prototype.rotateDot_ = function(dot, theta, origin) {
    // translate to origin
    dot.x -= origin.x ;
    dot.y -= origin.y ;

    // perform rotation
    newPos = {
         x: dot.x*Math.cos(theta) - dot.y*Math.sin(theta),
         y: dot.x*Math.sin(theta) + dot.y*Math.cos(theta)
    } ;
    dot.x = newPos.x ;
    dot.y = newPos.y ;

    // translate back to center
    dot.x += origin.x ;
    dot.y += origin.y ;

    dot.render();
};

If you want to know, how I rotate the points please reference to this site and this one.

overlaytiler.Rotater.prototype.onMouseMove_ = function(e) {                                                 
    var dots = this.controlDots_;                                                                           
     var center = overlaytiler.Rotater.prototype.getCenter_(dots);                                           

     // get the location of the canvas relative to the screen                                                                                                        
     var rect = new Array() ;
     rect[0] = dots[0].canvas_.getBoundingClientRect() ;
     rect[1] = dots[1].canvas_.getBoundingClientRect() ;
     rect[2] = dots[2].canvas_.getBoundingClientRect() ;

     // calculate the relative center of the image
     var relCenter =  {
         x: (rect[0].left + rect[2].left) / 2,
         y: (rect[0].top + rect[2].top) / 2
     } ;

     // calculate a vector from the center to the bottom left of the image
     dotCorner = {
         x: rect[1].left - (rect[1].left - relCenter.x) * 2 - relCenter.x,
         y: rect[1].top - (rect[1].top - relCenter.y) * 2 - relCenter.y
     } ;                                                                                                   

     // calculate a vector from the center to the mouse position                                             
     mousePos = {                                                                                            
          x: e.clientX - relCenter.x,                                                                           
          y: e.clientY - relCenter.y                                                                            
     } ;                                                                                                      

     // calculate the angle between the two vector                                                           
     theta = calculateAngle(dotCorner, mousePos) ;                                                           

     // is the mouse-vector left of the dot-vector -> refer to the german math board                                                           
     if(dotCorner.y*mousePos.x - dotCorner.x*mousePos.y > 0) {                                               
          theta *= -1 ;                                                                                      
     }                                                                                                       

     // calculate new position of the dots and render them                                                   
     overlaytiler.Rotater.prototype.rotateDot_(dots[2], theta, center);                                      
     overlaytiler.Rotater.prototype.rotateDot_(dots[1], theta, center);                                      
     overlaytiler.Rotater.prototype.rotateDot_(dots[0], theta, center);                                      

     // Render                                                                                               
     this.render();                                                                                          

     this.cx = e.clientX;                                                                                    
     this.cy = e.clientY;                                                                                    
 };

You can see that I wrote some function for vector calculations (just to make the code more readable):

function calculateScalarProduct(v1,v2)
{
    return (v1.x * v2.x + v1.y * v2.y) ;
}

function calculateLength(v1)
{
    return (Math.sqrt(v1.x*v1.x + v1.y*v1.y)) ;
}

function calculateAngle(v1, v2)
{
    return (Math.acos(calculateScalarProduct(v1,v2) / (calculateLength(v1)*calculateLength(v2)))) ;
}

This is my working solution. Comment if you don't understand something, so I can make my answer more comprehensive.

Working example: JSBin

Wow, this was a tough one.