Search code examples
javascriptsvglatitude-longitude

Plotting a Latitude and Longitude Coordinate in SVG


Summary
I'm trying to read a .SCT file which is a custom file type that is created by a program named VRC. I use this file to plot lat long coordinates within my source area. The issue I am having is that most of the lat long coordinates are not being mapped correctly. It appears that most coordinates are mapping to some arbitrary point.

Background
Here is a sample of the code I'm currently using to convert and plot them into the SVG.js canvas.

CODE [index.js (at least the active part)]:

var coor = [];
let fixed = [];

var dms2dd = function(object) {
  var deg = parseFloat(object.degrees),
      min = parseFloat(object.minutes),
      sec = parseFloat(object.seconds),
      dir = object.dir == "N" || object.dir == "E" ? 1 : -1;

  return dir*(deg+(min/60.0)+(sec/3600.0));
};

function llToXY(arr) {
    mapWidth    = 1000;
    mapHeight   = 1000;

    // get x value
    x = (arr[1]+180)*(mapWidth/360)

    // convert from degrees to radians
    latRad = arr[0]*Math.PI/180;

    // get y value
    mercN = Math.log(Math.tan((Math.PI/4)+(latRad/2)));
    y     = (mapHeight/2)-(mapWidth*mercN/(2*Math.PI));

    return [x, y];
}

lineReader.eachLine('test.txt', function(line, last) {
    let data = line.split(" ");
    let coor_length = coor.length;

    if (line[0] != " ") {
        coor.push({
            type: data[0],
            coordinates: []
        });

        // Convert to DD
        /*let direction = data[1].substr(0, 1);
        let dms = data[1].split(".")
        dms2dd({

        })*/


        data.splice(0, 1);

        coor[coor.length - 1]["coordinates"].push(data.join(" "));
    } else {
        coor[coor_length - 1]["coordinates"].push(line.trim())
    }

    if (last) {
        coor.forEach((data, index) => {
            for (coordinate_pair in data["coordinates"]) {
                let pair = data["coordinates"][coordinate_pair];

                pair = pair.split(" ");

                let x_data = pair[0].split("."),
                    y_data = pair[1].split(".");

                let x = dms2dd({
                    degrees: x_data[0].substring(1),
                    minutes: parseFloat(x_data[1]),
                    seconds: parseFloat(`${x_data[2]}.${x_data[3]}`),
                    dir: x_data[0].substr(0,1)
                });

                let y = dms2dd({
                    degrees: y_data[0].substring(1),
                    minutes: parseFloat(y_data[1]),
                    seconds: parseFloat(`${y_data[2]}.${y_data[3]}`),
                    dir: y_data[0].substr(0,1)
                });

                console.log([x, y]);
                coor[index]["coordinates"][coordinate_pair] = llToXY([x, y]);
            }
        })

        return false;
    }
});

Drawing Code

let draw = SVG("drawing").size(1000, 1000).panZoom();
            let cp = <%- JSON.stringify(cp) %>;
            //var line = draw.plot([32.737396,117.204284], [32.736862,117.204468], [32.737396,117.204284], [32.736862,117.204468]).stroke({ width: 1 })
            // var line = draw.polyline().fill("none").stroke({width: 0.00005});
            // line.transform({
            //     scale: 50000
            // }).transform({rotation: 104.5});

            cp.forEach((data)=> {
                //draw.polyline(data.coordinates.join(" "))
                //llToXY(data.coordinates);
                draw.polyline(data.coordinates.join(" ")).fill("none").stroke({width: 0.00005}).transform({scale: 50000}).transform({rotation: -15.80})
            });

This code is basically reading from a text file line-by-line and inserting the data into a variable named coor. Once the code reaches the last line, it will convert all coordinates i.e. coor values to decimal degrees.

Unfortunately, the library is not compatible with JSFiddle so I couldn't make a test scenario. I've attached all the necessary files, so you can run locally.

My main concern is that all coordinates are mapping to some arbitrary point.

Sources
This is what the image should look like enter image description here

What it currently looks like: enter image description here

VRC Sector File Documentation: http://www1.metacraft.com/VRC/docs/doc.php?page=appendix_g

StackOverflow Question Referenced: Convert latitude/longitude point to a pixels (x,y) on mercator projection

Library Used
svg.js: https://svgjs.dev/
svg.panzoom.js: https://github.com/svgdotjs/svg.panzoom.js


Solution

  • That is actually an issue with bad documentation for the SVG.js library. If you define a transformation as

    element.transform({ scale: 30000 })
    

    the result is not a transform attribute with the value scale(30000), which would mean an origin for the scaling at point (0, 0), but a transform matrix that is equivalent to a scaling around the center of the element bounding box.

    In your code, each partial shape is drawn as a separate polyline, and is separately scaled around its individual center. The element, before the scaling, is extremely small, and all elements are as closely grouped together as to be virtually at one point. If they are scaled , the result looks like all elements have that point as one common center at their new size.

    The most obvious solution is to scale the elements not around their individual center, but around one constant value:

    const cx = ..., cy = ...
    element.transform({ scale: 30000, cx, cy })
    

    What that value is is not immediately clear. It would be a point that is in the center of the common bounding box of all polylines. How do you get at that? Let the library do the work for you.

    If you add all polylines as childs of a <g> element, you can scale that group, and if you leave out values for the center, they will be computed for you:

    let draw = SVG("drawing").size(1000, 1000).panZoom();
    let g = draw.group();
    
    let cp = <%- JSON.stringify(cp) %>;
    cp.forEach((data) => {
        group.polyline(data.coordinates.join(" "))
           .fill("none")
           .stroke({width: 0.00005});
    });
    
    group.transform({scale: 50000}).transform({rotation: -15.80});
    

    The above solution is good if you want to get your resulting map at a defined size. If you want to find a scale value that actually lets the content fill your canvas from side to side, it is just as simple: you can get the bounding box of of the group, and set them as a viewbox on the <svg> element. The browser will then take care to scale that box to fit the canvas.

    let draw = SVG("drawing").size(1000, 1000).panZoom();
    let g = draw.group();
    
    let cp = <%- JSON.stringify(cp) %>;
    cp.forEach((data) => {
        group.polyline(data.coordinates.join(" "))
           .fill("none")
           .stroke({width: 0.00005});
    });
    
    const { x, y, w, h } = group.bbox();
    draw.viewbox(x, y w, h);