Home > Blockchain >  (js) SVG cartography : how to detect border countries
(js) SVG cartography : how to detect border countries

Time:12-24

SVG World Map

I am using SVG elements to render this world map in my web app. SVG structure is classic :

<svg id="all_countries">
  <path
    id="india"
    className="st0" // st0 is the default class when exporting svg from Adobe Illustrator
    d="M666.8556,380.5002l0.518,0.3002l-0.0375,-0.0375-0.0891l-0.201-0.1823l0" // country svg drawing
  />

  <path
    id="china"
    className="st0"
    d="M680.55626,380.402l0.518,0.3002l-0.0375,-0.075-0.0591l-0.301-0.3484l2"
  />

  // more countries like Nepal, Kazakhstan, Mongolia, Pakistan etc ...
</svg>

For every country : I am looking for a way to find the border countries. In the following example, all countries are stored in database and have properties :

svgID: "india",
prettyName: "Republic Of India",
population: "1393000000",
currency: "₹",
school: {
  forKids: "free",
  forAdults: "free",
},

I need to add a borderCountries property :

borderCountries: [
  "new ObjectId("2482842848451")", // represents Pakistan
  "new ObjectId("5848564841232")", // represents Nepal
  "new ObjectId("9645485455612")", // represents China
]

I thought about adding them manually to database but (later) I'm adding regions inside countries, and that would be too much manual work.

Any thoughts on how to detect border countries using the SVG elements ?

CodePudding user response:

You can find border/neighbor states via natively supported js method isPointInStroke().

let svg = document.querySelector('svg');
let states = svg.querySelectorAll('path');
let checks = 0;
perfStart();

/**
* collect data in info array
* find neighbours by pointInStroke checks
*/
let stateInfo = [];
let checksPerPath = 12;

states.forEach((state, s) => {
    let id = state.id;
    if (!stateInfo[s]) {
        let bb = state.getBBox();
        stateInfo.push({
            id: id,
            neighbours: [],
            bb: [bb.x, bb.y, bb.x   bb.width, bb.y   bb.height],
            pathLength: state.getTotalLength()
        });
    }
    let [x, y, right, bottom] = stateInfo[s].bb;
    let pathLength = stateInfo[s].pathLength;

    for (let i = 0; i < states.length; i  ) {
        let state1 = states[i];
        let id = state1.id;
        if (!stateInfo[i]) {
            let bb = state1.getBBox();
            stateInfo.push({
                id: id,
                neighbours: [],
                bb: [bb.x, bb.y, bb.x   bb.width, bb.y   bb.height],
                pathLength: state1.getTotalLength()
            });
        }

        let [x1, y1, right1, bottom1] = stateInfo[i].bb;
        let pathLength1 = stateInfo[i].pathLength;

        /**
        * narrow down selection by checking  bbox intersections
        */
        if (
            s != i &&
            x <= right1 &&
            right >= x1 &&
            y <= bottom1 &&
            bottom >= y1) {

            /**
            * refine by point in fill checks
            * skip previously compared paths 
            * (e.g already processed A-B combination makes comparing B-A obsolete) 
            */
            if (!stateInfo[s].neighbours.includes(state1.id) && !stateInfo[i].neighbours.includes(state.id)) {
                let inStroke = false;
                let inStroke1 = false;
                for (let c = 0; c < checksPerPath && !inStroke && !inStroke1; c  ) {
                    let pt = state.getPointAtLength(pathLength / checksPerPath * c);
                    inStroke = state1.isPointInStroke(pt)
                    // check path 1 against path 2
                    if (inStroke) {
                        stateInfo[s].neighbours.push(state1.id);
                        stateInfo[i].neighbours.push(state.id);
                    }else{
                    // no intersections found: reverse order
                        let pt1 = state1.getPointAtLength(pathLength1 / checksPerPath * c);
                        inStroke1 = state.isPointInStroke(pt1)
                        if (inStroke1) {
                            stateInfo[s].neighbours.push(state1.id);
                            stateInfo[i].neighbours.push(state.id);
                        }
                    }
                    // just for benchmarking
                    checks  
                }
            }
        }
    }
    let title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
    let neighboursAtt = stateInfo[s].neighbours.join(', ');
    title.textContent = 'Neighbour states: '   neighboursAtt;
    state.appendChild(title);
    state.dataset.neighbours = neighboursAtt;
});


perfEnd();
console.log('total checks:', checks, stateInfo);

/**
 * highlight neighbours on click: test result
 */
states.forEach(function (state, i) {
    state.addEventListener('click', function (e) {
        removeNeighbourClass();
        let current = e.currentTarget;
        current.classList.add('active');
        let neighbours = e.currentTarget.getAttribute('data-neighbours');
        if (neighbours) {
            let neighboursArr = neighbours.split(', ');
            if (neighboursArr.length) {
                neighboursArr.forEach(function (el, i) {
                    let neighbour = svg.querySelector('#'   el);
                    neighbour.classList.add('neighbour');
                })
            }
        }
    });
});

function removeNeighbourClass() {
    let neighbours = document.querySelectorAll('.neighbour, .active');
    neighbours.forEach(function (el, i) {
        el.classList.remove('active');
        el.classList.remove('neighbour');
    })
}


/**
 * simple performance test
 */

function perfStart() {
    t0 = performance.now();
}

function perfEnd(text = '') {
    t1 = performance.now();
    total = t1 - t0;
    console.log(`excecution time ${text}:  ${total} ms`);
    return total;
}


function renderPoint(svg, coords, fill = "red", r = "2", opacity = "1", id = "", className = "") {
    //console.log(coords);
    if (Array.isArray(coords)) {
      coords = {
        x: coords[0],
        y: coords[1]
      };
    }
  
    let marker = `<circle  opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
    <title>${coords.x} ${coords.y}</title></circle>`;
  svg.insertAdjacentHTML("beforeend", marker);
  }
svg{
max-height:90vmin;
width:auto;
}


.active{
      fill:blue
}
.neighbour{
    fill:#5774ad
}
<p>Click on states to highlight neighbour states</p>
<svg xmlns="http://www.w3.org/2000/svg" style="stroke-linejoin:round;stroke:#000;fill:#ccc" width="1000" viewBox="0 0 200 250" id="svg">
  <path id="MA" d="m162.3 142.4-.291-.193v.29l.29-.096zm-2.912-2.62.68-.291v-.388l-.68.68zm12.04-7.57-.097-1.36-.194-.776.29 2.135zm-42.42-9.998-.68.29-5.532 1.651-1.941.68-2.233.679-.776.291v.291l.291 5.048.291 4.659.291 4.27.486.292 1.747-.486 7.862-2.33.194.486 13.98-5.338.097.194 1.262-.486 4.465-1.747 4.27 5.145.583-.486.291-1.456-.097 2.33h.97l.292 1.165.874 1.65 4.562-5.533 3.785 1.262.874-1.941 6.212-3.3-2.621-5.145.68 3.3-3.204 2.427-3.591.291-7.183-7.668-3.203-4.853 3.203-3.397-3.3-.194-1.359-3.204-.097-.194-5.532 6.018-12.23 4.077z" data-id="MA" data-name="Massachusetts"/>
  <path id="CT" d="m143 132.7-13.98 5.338-.194-.485-7.862 2.33-1.747.485.194.97 3.688 14.27 1.165 1.456-2.524 3.106 1.748 1.65 5.63-5.435 3.008-4.174.583 1.165 14.17-6.31.292-.29-.097-2.33-.583-2.038-3.008-8.445-.389-1.067z" data-id="CT" data-name="Connecticut"/>
  <path id="NH" d="m129.9 65.29-1.262 1.553-1.456-.291-.777 6.406v.097l2.33 8.735-.194 1.748-4.174 5.532 1.068 5.047v6.31l-.777 5.823 4.368 15.92 3.981-1.262 12.23-4.077 5.532-6.018v-1.553l.291-2.523-.679.29-.097-.582-6.115-7.473-.194-.68-14.07-33z" data-id="NH" data-name="New Hampshire"/>
  <path id="RI" d="m152.5 141.4-.097-2.038-.29.776zm1.942-1.553-1.262-2.426-.097 2.523zm-.583-3.009.68 1.747.873 1.553.583-1.164-.874-1.65-.291-1.165h-.97v.68zm-10.77-3.98.389 1.068 3.009 8.445.582 2.038.097 2.33.194-.291 4.66-3.689-.874-6.6 1.94-.388-4.27-5.145-4.465 1.747-1.262.486z" data-id="RI" data-name="Rhode Island"/>
  <path id="VT" d="m129 122.2-4.369-15.92.777-5.823v-6.31l-1.068-5.047 4.174-5.532.194-1.748-2.33-8.735-1.261.485-5.339 1.941-5.241 2.039-12.04 4.368-.68.194 4.368 10.19 1.456 9.61 5.145 8.056 4.853 15.92.194-.097.776-.291 2.233-.68 1.941-.68 5.533-1.65z" data-id="VT" data-name="Vermont"/>
  <path id="NJ" d="m122.8 196.6-.485-1.166v1.165h.485zm.97-3.592.292-2.912-.582 2.524zm-3.979-29.8-10.19-2.523-1.748-.486-1.65-.388-4.076 9.027 1.747 2.135-.097 6.504 1.844.29 3.785 9.901-.194.292-4.756 6.988.485 5.339 11.45 3.882.097 4.465 2.62-3.688.389-5.339 2.718-1.844-1.068-3.98 2.038-4.756v-10.39l-.68-3.397-4.561.097 1.165-4.659.68-7.474z" data-id="NJ" data-name="New Jersey"/>
  <path id="NY" d="m119.5 171.1-1.456 2.038-.388.582zm13.4-4.757 4.853-4.27v-.194zm-12.72 3.01v-3.3l-.29 1.746.29 1.553zm22.52-15.53-1.165-.292-.194.874 1.359-.583zm2.62-1.748-.096.97.194-.582-.097-.388zm-2.717.097-6.503 6.018-9.998 4.271-2.62 2.232-.195 2.136-2.717 2.232.388 2.912 4.174-1.65 6.115-4.95 6.794-3.591 10.29-9.707-7.28 5.533-2.33.97zm3.009-4.076.388-.583-1.165 1.36zm-109.1-3.592-1.553-.194 1.165 1.553zm32.22-29.7.194-1.068-.776.97zm.485-6.989-.388.195-.485 1.067zm-41.06 55.13.679 2.912 5.144 1.262 29.51-8.056 29.7-8.93 5.436 4.174 1.068 2.426 6.31 2.718.193.388 1.65.389 1.748.485 10.19 2.523-.194-.485.874 3.98 1.94-.486 1.069-4.367v-.098l-1.748-1.65 2.524-3.106-1.165-1.456-3.688-14.27-.194-.97-.486-.292-.29-4.27-.292-4.66-.291-5.047v-.29l-.194.096-4.853-15.92-5.145-8.056-1.456-9.61-4.368-10.19-.582.29-20.29 6.795-4.077 4.368-4.562 8.736.194 1.941-6.018 9.124 3.689 1.262 1.068 6.891-4.271 5.63-12.81 7.765-2.426-.97-9.512 1.358-8.833 5.339.485 2.815 3.106 2.717-1.747 8.542-6.988 8.153z" data-id="NY" data-name="New York"/>
  <path id="PA" d="m106 159.4-6.309-2.718-1.068-2.426-5.435-4.174-29.7 8.93-29.51 8.056-5.144-1.262-.68-2.912-1.65 1.262-2.62 1.844-4.756 4.853-.583.486 2.718 11.94.485 2.136 2.427 10.48.485 2.038 4.271 18.44 13.78-3.397 1.941-.486 1.747-.485 6.892-1.844 37.95-10.58 6.891-2.135 1.65-.486.777-1.94 1.747-1.36h2.33l.29-.873 3.786-4.271.388-.68.291-.194-3.785-9.9-1.844-.291.097-6.504-1.747-2.135 4.076-9.027z" data-id="PA" data-name="Pennsylvania"/>
  <path id="ME" d="m152.8 107.7h-.097l.194.096-.097-.097zm7.182-20.67-.776-1.165v.582zm-1.359-1.068v-.97l-.097-.583zm3.495-4.076-.194-.098.097.292zm9.706-4.854-.194-1.456-.388.68zm-2.427.583.097-1.068-1.164.194zm3.592-3.203h-.389v.194zm-4.853 1.456-.874-.195-.097.874.97-.68zm4.95-3.01.873.68-.29-.776-.583.097zm-2.62.098-.874 1.65 1.164-.583zm-4.175.097-.388.582.291.874zm5.727-3.203-.097-.777-.291.486.388.29zm-.485.388-.874-.777v.68zm-5.436 2.232-.194-1.067-.388.873zm9.221-3.397-1.456 2.136-.097-1.553zm7.086-6.503-.68-.97-.388.193zm2.62-4.368-.582-.291-.097.291h.68zm1.262-7.57h-.873l.97.29zm1.456.97-.29-1.747-.777-.097zm-1.553-1.553-.194-1.844-.68 1.261zm-56.3 15.24 14.07 33 .194.68 6.115 7.473v-.097l.97-.29.195-5.533 1.553-1.942.97-5.144-.873-.874 1.65-5.047 5.63-2.62 3.688-4.465 1.359-7.766 5.92-.388-.776-3.98 7.377-2.814.485-4.368 1.747.388 4.854-3.98 2.135-5.144-4.95-4.95-5.242-1.068-1.94-3.98v-2.329l-3.204.97-3.591-1.26v-3.98l-9.707-20.97-7.085-3.689-7.862 6.6-2.136-.387-1.844-3.398-2.524.97-3.591 17.86 1.262 5.824 1.068 11.84-2.815 9.124 1.941 1.456-2.33 4.465-2.523-.388z" data-id="ME" data-name="Maine"/>
</svg>

<p><a href="https://simplemaps.com/resources/svg-us">https://simplemaps.com/resources/svg-us</a></p>

How it works

  • we narrow down the selection of possible neighbors by comparing bounding box coordinates – retrieved with getBBox()
  • you're moving around each <path> and check isPointInStroke() intersections at certain points
  • these checkpoints intervals are calculated by getPointAtLength()
    getPointAtLength() calls can significantly impact performance when run hundreds of times. That's why you should always try to reduce the number of intervals. In the above example we're calculating 12 points per path. Depending on the path geomerty (e.g if a path is highly concave) you might need to increase the number of points.
  • save your results either in a data object/array or in a data attribute (this way border countries can be stored statically in your svg file).
  • Related