Search code examples
javascriptangulartypescriptleafletangular-leaflet-directive

Transform X and Y coordinates inside a (rotated) ImageOverlay to latitude and longitude


after reading through Projection's documentation, and some fail-and-try testing, i have found myself unable to find a solution to this issue.

Al the solutions i find play with the fact that the x and y coordinates are in reference to the mercator map projection, but in this case i have them from a local image placed on a layer over the map.

Let's assume i have a 400px * 200px grey image, placed on a leaflet map using ImageOverlay.Rotated, at the position:

var topleft = L.latLng(41.604766, 0.606402);
var topRight = L.latLng(41.605107, 0.606858);
var bottomleft = L.latLng(41.604610, 0.606613);

This image is placed on the map using top left, top right, and bottom left coordinates as reference.

the result is a rectangle slightly rotated to the north.

If i knew i left my phone at the pixel 100,50 ( the left corner of the original image ), how could i transform this cartesian coordinate into a latitude and longitude for my map to display it?

if it was a rectangle placed perfectly aligned to the north, i guess i could get the difference in º between the topLeft and bottomLeft corners, divide it by number of pixels, and then multiply the result with my phone's Y to get the latitude. using the prior value, i'd multiply it to my phone's X to get my phone's longitude.

but given the shape is rotated, the world is not flat, and i was really bad at geography and trigonometry, i'm not sure about it being precise enough.

in the end we're speaking of a phone, not a building. a few meters off ( 2-4) is okay, a building block away is not.


Solution

  • Oh, you're talking about my very own Leaflet.ImageOverlay.Rotated. I'll be happy to oblige.

    You should have a look at its source code. All the maths are there. Let's go over some bits of that code:

        var pxTopLeft    = this._map.latLngToLayerPoint(this._topLeft);
        var pxTopRight   = this._map.latLngToLayerPoint(this._topRight);
        var pxBottomLeft = this._map.latLngToLayerPoint(this._bottomLeft);
    

    This is converting the three lat-long coordinates into screen-relative pixel coordinates. The Leaflet tutorial about creating subclasses of L.Layer has a good explanation of what "layer point" means.

    You can replace these calculations to any other reprojections you want, assuming that you're working in a plane and not on a geoid. In other words: you might want to transform your lat-long coordinates into EPSG:3857 spherical mercator coordinates (which is the display projection Leaflet uses). If you do so, you can later convert an interpolated EPSG:3857 coordinate into a EPSG:4326 "plain latitude-longitude" coordinate.

        // Calculate the skew angles, both in X and Y
        var vectorX = pxTopRight.subtract(pxTopLeft);
        var vectorY = pxBottomLeft.subtract(pxTopLeft);
    

    The CSS transforms need the skew angles to distort the image properly. It is perhaps counter-intuitive, but ImageOverlay.Rotated uses CSS's skew() rather than rotate. But you don't need the skew angles, you just need those differential vectors.

    If you want to visualize that, vectorX is a 2D vector that goes along the top side of your image (from left to right), and vectorY is a 2D vector that goes along the left side (from top to bottom).

    Those vectors allow you to get the screen coordinate of any point in the image, assuming an input in the range of ([0..1], [0..1]) (with 0 being the top or left of the image, and 1 being the bottom or the right). But you probably don't want to work in numbers relative to the height/width of the image, because in your question you mention pixels. So let's grab pixels.

        var imgW = this._rawImage.width;
        var imgH = this._rawImage.height;
    

    Good. We have extracted from ImageOverlay.Rotated all the bits of math required.


    Now, if you divide the previous vectors by the image dimensions in pixels...

        var vectorXperPx = vectorX.divideBy(imgW);
        var vectorYperPx = vectorY.divideBy(imgW);
    

    These vectors go from one pixel of your original image to a pixel either on its left (x) or underneath (y). So given a pixel (x,y) of the original image, the projected vector relative to the image corner will be:

        var deltaVector = 
                   xPixelCoordinate.scaleBy(vectorXPerPx)
              .add(yPixelCoordinate.scaleBy(vectorYPerPx))
    

    That's the vector, in screen coordinates, from the top-left corner of your image to the (xPixelCoordinate, yPixelCoordinate) pixel of your image.

    Now add the screen coordinate of the top-left corner of the image, and you're set:

        var finalCoordinateForImagePixel = pxTopLeft.add(deltaVector);
    

    As we used latLngToLayerPoint, the result will be relative to that frame of reference. Want to get it back? Easy:

        var finalCoordinateForImagePixelInLatLong = 
               map.layerPointToLatLng(finalCoordinateForImagePixel);
    

    but given the shape is rotated, the world is not flat, and i was really bad at geography and trigonometry, i'm not sure about it being precise enough.

    If you use the coordinate reference system that Leaflet uses for display (EPSG:3857), or any homomorphic coordinate systems (pixel coordinates relative to the screen, pixel coordinates relative to the layer origin point), you'll have no problem.