Search code examples
javascriptgoogle-mapsimage-manipulationimage-rotation

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/


Solution

  • 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.