2619 2619 - 2 months ago 9
Javascript Question

find country coordinates from array

I am writing a small app. I have following data

{
"feat": [
{
"type": "Feature",
"id": "AFG",
"properties": {
"name": "Afghanistan"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
61.210817,
35.650072
],
[
64.546479,
36.312073
],
[
64.746105,
37.111818
],
[
65.588948,
37.305217
],
[
65.745631,
37.661164
],
[
66.217385,
37.39379
],
[
66.518607,
37.362784
],
[
67.075782,
37.356144
],
[
67.83,
37.144994
],
[
68.135562,
37.023115
],
[
68.859446,
37.344336
],
[
69.196273,
37.151144
],
[
69.518785,
37.608997
],
[
70.116578,
37.588223
],
[
70.270574,
37.735165
],
[
71.262348,
36.074388
],
[
71.498768,
35.650563
],
[
71.613076,
35.153203
],
[
71.115019,
34.733126
],
[
71.156773,
34.348911
],
[
70.881803,
33.988856
],
[
69.930543,
34.02012
],
[
70.323594,
33.358533
],
[
69.687147,
33.105499
],
[
69.262522,
32.501944
],
[
69.317764,
31.901412
],
[
68.926677,
31.620189
],
[
68.556932,
31.71331
],
[
67.792689,
31.58293
],
[
60.52843,
33.676446
],
[
60.803193,
34.404102
],
[
61.210817,
35.650072
]
]
]
}
},
{
"type": "Feature",
"id": "AGO",
"properties": {
"name": "Angola"
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[
16.326528,
-5.87747
],
[
16.57318,
-6.622645
],
[
16.860191,
-7.222298
],
[
17.089996,
-7.545689
],
[
17.47297,
-8.068551
],
[
21.801801,
-8.908707
],
[
21.875182,
-9.523708
],
[
22.208753,
-9.894796
],
[
22.155268,
-11.084801
],
[
22.402798,
-10.993075
],
[
22.837345,
-11.017622
],
[
23.456791,
-10.867863
],
[
23.912215,
-10.926826
],
[
24.017894,
-11.237298
],
[
23.904154,
-11.722282
],
[
24.079905,
-12.191297
],
[
23.930922,
-12.565848
],
[
24.016137,
-12.911046
],
[
21.933886,
-12.898437
],
[
21.887843,
-16.08031
],
[
22.562478,
-16.898451
],
[
23.215048,
-17.523116
],
[
21.377176,
-17.930636
],
[
18.956187,
-17.789095
],
[
18.263309,
-17.309951
],
[
12.175619,
-14.449144
],
[
12.500095,
-13.5477
],
[
12.738479,
-13.137906
],
[
13.312914,
-12.48363
],
[
13.633721,
-12.038645
],
[
13.738728,
-11.297863
],
[
13.686379,
-10.731076
],
[
13.387328,
-10.373578
],
[
13.120988,
-9.766897
],
[
12.87537,
-9.166934
],
[
13.375597,
-5.864241
],
[
16.326528,
-5.87747
]
]
],
[
[
[
12.436688,
-5.684304
],
[
12.182337,
-5.789931
]
]
]
]
}
},
{
"type": "Feature",
"id": "ALB",
"properties": {
"name": "Albania"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
20.590247,
41.855404
],
[
19.406082,
40.250773
],
[
19.319059,
40.72723
],
[
19.40355,
41.409566
],
[
19.540027,
41.719986
],
[
19.371769,
41.877548
],
[
19.304486,
42.195745
],
[
19.738051,
42.688247
],
[
19.801613,
42.500093
],
[
20.0707,
42.58863
],
[
20.283755,
42.32026
],
[
20.52295,
42.21787
],
[
20.590247,
41.855404
]
]
]
}
}
]
}


What happens is that user will provide
latitude
and
longitude
. Then I need to find if those values are present in above dataset. if it is then return that country.

The problem is that above dataset coordinates array is different. Sometimes it is 3 dimensional and sometimes it is 2.It is keep changing. I am not sure how can I find efficient solution for this.

Below is some of my code which currently it prompts user from latitude and longitude and and loop through data.

const rl = require('readline').createInterface(process.stdin, process.stdout);
const coordinates = require('./data.json').feat;

var prompts = ['Enter coordinates (lat, lon)'],
counter = 0;

// console.log(countries);

rl.on('line', (line) => {
if (!line) {
console.log('Enter values.');
}

let lat = line.split(' ')[0],
lon = line.split(' ')[1];

console.log(lat, lon);

coordinates.forEach((data, index) => {
console.log(data.geometry.coordinates[index]);
data.geometry.coordinates.forEach((geo, i) => {
// console.log(geo[i]);
});
});

get();
}).on('close', () => {

});

function get() {
rl.setPrompt(prompts[counter] + ': ');
rl.prompt();
}

get();


Any help in finding given longitude and latitude in above dataset?

Answer

You can detect the number of polygons by checking the type property. If it is MultiPolygon, you'll have one more nested level.

Here are some functions you could use to determine whether a point is in a polygon. The rest (lower part) of the snippet allows to test the getCountry function: it performs the map drawing, mouse move handling and showing the country which the mouse moves over. But the essence is in the three functions at the top.

This is plain JS, based on the winding algorithm presented in this article but you might want to consider using a library for that instead, like d3.js.

function isLeft(p0, p1, p2) {
    return ( (p1[0] - p0[0]) * (p2[1] - p0[1])
           - (p2[0] - p0[0]) * (p1[1] - p0[1]) );
}

function inPoly(p, coord) {
    var winding = 0;
    // loop through all edges of the polygon
    for (var i=0; i<coord.length-1; i++) {
        if (coord[i][1] <= p[1]) {
            if (coord[i+1][1]  > p[1])     // an upward crossing
                 if (isLeft(coord[i], coord[i+1], p) > 0)  // p left of edge
                     ++winding;            // have  a valid up intersect
        } else {                           // start y > P.y (no test needed)
            if (coord[i+1][1] <= p[1])     // a downward crossing
                 if (isLeft(coord[i], coord[i+1], p) < 0)  // p right of  edge
                     --winding;            // have  a valid down intersect
        }
    }
    return winding;
}

function getCountry(p, data) {
    // p: point represented by array with the two coordinates
    var country;
    return data.feat.some(function (obj) {
        country = obj;
        var polygons = obj.geometry.coordinates;
        if (obj.geometry.type !== 'MultiPolygon') polygons = [polygons];
        return polygons.some(function (polygon) {
            return inPoly(p, polygon[0]);
        });
    }) ? country : null;
}

// I/O for this interactive snippet

function drawPolygon(ctx, coord, fillColor) {
    ctx.fillStyle = fillColor;
    ctx.beginPath();
    coord.forEach(function (point, i) {
        if (i==0) {
            ctx.moveTo(point[0], point[1]);
        } else {
            ctx.lineTo(point[0], point[1]);
        }
    });
    ctx.fill();
    ctx.stroke();
//    ctx.closePath();
}

function drawCountry(ctx, selectedCountry, country) {
    var polygons = country.geometry.coordinates;
    var color = selectedCountry == country ? 'yellow' : 'grey';
    if (country.geometry.type !== 'MultiPolygon') polygons = [polygons];
    polygons.forEach(function (polygon) {
        drawPolygon(ctx, polygon[0], color);
    });
}

function drawWorld(ctx, data, selectedCountry) {
    data.feat.forEach(drawCountry.bind(null, ctx, selectedCountry));
}

// Define transformation so countries will be mapped to canvas area
var scale = [3, -3];
var move = [-20, 130];

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
ctx.translate(...move);
ctx.scale(...scale);
ctx.lineWidth = 1/scale[0];

// Sample data
var data = {"feat":[{"type":"Feature","id":"AFG","properties":{"name":"Afghanistan"},"geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],[62.230651,35.270664],[62.984662,35.404041],[63.193538,35.857166],[63.982896,36.007957],[64.546479,36.312073],[64.746105,37.111818],[65.588948,37.305217],[65.745631,37.661164],[66.217385,37.39379],[66.518607,37.362784],[67.075782,37.356144],[67.83,37.144994],[68.135562,37.023115],[68.859446,37.344336],[69.196273,37.151144],[69.518785,37.608997],[70.116578,37.588223],[70.270574,37.735165],[70.376304,38.138396],[70.806821,38.486282],[71.348131,38.258905],[71.239404,37.953265],[71.541918,37.905774],[71.448693,37.065645],[71.844638,36.738171],[72.193041,36.948288],[72.63689,37.047558],[73.260056,37.495257],[73.948696,37.421566],[74.980002,37.41999],[75.158028,37.133031],[74.575893,37.020841],[74.067552,36.836176],[72.920025,36.720007],[71.846292,36.509942],[71.262348,36.074388],[71.498768,35.650563],[71.613076,35.153203],[71.115019,34.733126],[71.156773,34.348911],[70.881803,33.988856],[69.930543,34.02012],[70.323594,33.358533],[69.687147,33.105499],[69.262522,32.501944],[69.317764,31.901412],[68.926677,31.620189],[68.556932,31.71331],[67.792689,31.58293],[67.683394,31.303154],[66.938891,31.304911],[66.381458,30.738899],[66.346473,29.887943],[65.046862,29.472181],[64.350419,29.560031],[64.148002,29.340819],[63.550261,29.468331],[62.549857,29.318572],[60.874248,29.829239],[61.781222,30.73585],[61.699314,31.379506],[60.941945,31.548075],[60.863655,32.18292],[60.536078,32.981269],[60.9637,33.528832],[60.52843,33.676446],[60.803193,34.404102],[61.210817,35.650072]]]}},{"type":"Feature","id":"AGO","properties":{"name":"Angola"},"geometry":{"type":"MultiPolygon","coordinates":[[[[16.326528,-5.87747],[16.57318,-6.622645],[16.860191,-7.222298],[17.089996,-7.545689],[17.47297,-8.068551],[18.134222,-7.987678],[18.464176,-7.847014],[19.016752,-7.988246],[19.166613,-7.738184],[19.417502,-7.155429],[20.037723,-7.116361],[20.091622,-6.94309],[20.601823,-6.939318],[20.514748,-7.299606],[21.728111,-7.290872],[21.746456,-7.920085],[21.949131,-8.305901],[21.801801,-8.908707],[21.875182,-9.523708],[22.208753,-9.894796],[22.155268,-11.084801],[22.402798,-10.993075],[22.837345,-11.017622],[23.456791,-10.867863],[23.912215,-10.926826],[24.017894,-11.237298],[23.904154,-11.722282],[24.079905,-12.191297],[23.930922,-12.565848],[24.016137,-12.911046],[21.933886,-12.898437],[21.887843,-16.08031],[22.562478,-16.898451],[23.215048,-17.523116],[21.377176,-17.930636],[18.956187,-17.789095],[18.263309,-17.309951],[14.209707,-17.353101],[14.058501,-17.423381],[13.462362,-16.971212],[12.814081,-16.941343],[12.215461,-17.111668],[11.734199,-17.301889],[11.640096,-16.673142],[11.778537,-15.793816],[12.123581,-14.878316],[12.175619,-14.449144],[12.500095,-13.5477],[12.738479,-13.137906],[13.312914,-12.48363],[13.633721,-12.038645],[13.738728,-11.297863],[13.686379,-10.731076],[13.387328,-10.373578],[13.120988,-9.766897],[12.87537,-9.166934],[12.929061,-8.959091],[13.236433,-8.562629],[12.93304,-7.596539],[12.728298,-6.927122],[12.227347,-6.294448],[12.322432,-6.100092],[12.735171,-5.965682],[13.024869,-5.984389],[13.375597,-5.864241],[16.326528,-5.87747]]],[[[12.436688,-5.684304],[12.182337,-5.789931],[11.914963,-5.037987],[12.318608,-4.60623],[12.62076,-4.438023],[12.995517,-4.781103],[12.631612,-4.991271],[12.468004,-5.248362],[12.436688,-5.684304]]]]}},{"type":"Feature","id":"ALB","properties":{"name":"Albania"},"geometry":{"type":"Polygon","coordinates":[[[20.590247,41.855404],[20.463175,41.515089],[20.605182,41.086226],[21.02004,40.842727],[20.99999,40.580004],[20.674997,40.435],[20.615,40.110007],[20.150016,39.624998],[19.98,39.694993],[19.960002,39.915006],[19.406082,40.250773],[19.319059,40.72723],[19.40355,41.409566],[19.540027,41.719986],[19.371769,41.877548],[19.304486,42.195745],[19.738051,42.688247],[19.801613,42.500093],[20.0707,42.58863],[20.283755,42.32026],[20.52295,42.21787],[20.590247,41.855404]]]}}]};
// Show map on canvas
window.onload = drawWorld.bind(null, ctx, data, null);

// Handle mouse move to mention the matching country
canvas.onmousemove = function (e) {
    // Get mouse coordinates relative to canvas
    var x = e.pageX - this.offsetLeft;
    var y = e.pageY - this.offsetTop;
    // Perform the reverse of the canvas transformation
    x = (x - move[0]) / scale[0];
    y = (y - move[1]) / scale[1];
    // Get country that contains this point
    var country = getCountry([x, y], data);
    var countryName = country ? country.properties.name : 'no match';
    // Output the result
    document.getElementById('country').textContent = countryName;
    // Highight country
    drawWorld(ctx, data, country)
};
canvas { border:1px solid; float: left }
<canvas width="250", height="200"></canvas>
<div id="country">Hover mouse...</div>

Note about performance

When you have a large list of polygons in your data set, it really is a waste of time to check each individual edge of each polygon to determine where the point fits.

It will be a major improvement if you would preprocess the data and add box information to each polygon: a square that encompasses the polygon completely ([minx,miny]-[maxx,maxy]). When you need to match a point, you would first match against this box, and if it is outside, you can skip that polygon, saving time spent on it. There will normally only be a few of those boxes that encompass your point, and so you would only need to test against those few polygons.

To just get the name of the country:

Call the getCountry function like it is done in the above snippet, where you pass the user defined x and y (i.e. longitude, latitude) as an array, and the data object as the second argument:

var country = getCountry([x, y], data);
var countryName = country ? country.properties.name : 'no match';
Comments