使用大数组进行反向地理编码是最快的方法吗? - javascript 和性能

Reverse geocoding with big array is fastest way? - javascript and performance

我在 Google Maps 上有很多点,我想为每个点显示最近的城市(因此进行反向地理编码)。 我有一个这样的多维数组:

citta_vicine = [];

var comuni = [
 ["Abano Terme (PD)",45.3594,11.7894],
 ["Abbadia Cerreto (LO)",45.3122,9.5928],
 ["Abbadia Lariana (LC)",45.8992,9.3336],
 ["Abbadia San Salvatore (SI)",42.8800,11.6775],
 ["Abbasanta (OR)",40.1250,8.8200]
]

//city, latitude, longitude

问题是我的数组包含意大利的所有城市(8000!)并且是 300 Kb。

要获得最近的城市,我可以使用这个:

//this line will be inside for loop of points
 var city_near= estrapola_paese(50,lat,lng); //lat and lng are coordinates of these points
//


 function estrapola_paese(distanza,latB,longB){ 
  citta_vicine = [];
  for(var i= 0; i < comuni.length; i++){
    var dist_eqcity= dist_coords(comuni[i][1],comuni[i][2],latB,longB);
    if(dist_eqcity < distanza){
        citta_vicine.push([dist_eqcity, comuni[i][0]]);
    }
  }
  if(citta_vicine.length > 0){
    citta_vicine.sort(function(a, b) {
        return a - b;
    }); 
    return citta_vicine[0][1];
  }
  else{
    distanza = distanza+50;
    estrapola_paese(distanza,latB,longB);   
  }
 }

//calculate distance in Km between city and "point"
function dist_coords(latA,longA,latB,longB) {
 var R = 6372.795477598;
 var laA = latA * Math.PI/180;
 var laB = latB * Math.PI/180;
 var loA = longA * Math.PI/180;
 var loB = longB * Math.PI/180;
 var distanza = R * Math.acos(Math.sin(laA)*Math.sin(laB) + Math.cos(laA)*Math.cos(laB) * Math.cos(loA-loB));
 if(isNaN(distanza) == true){   
  distanza = 0;
 }
 return distanza;
} 

简而言之,对于性能问题,我(首先)只考虑距该点50公里半径范围内的城市。 如果 50 公里以内有城市,我将城市(和距离)添加到 "citta_vicine" 数组中,并将后一个数组从最低值到最高值排序。 因此距离最近的城市最远。

如果 50 公里内没有城市,那么我会再次执行函数 "estrapola_paese",但会增加半径以考虑另外 50 公里。


我认为 CODE WORKS 但我有很多疑问:

1) 文件大小为 459 KB:是不是太大了?

2) 有没有更好的方法来完成这一切?

3) 数组 citta_vicine 的排序是否正确? 如果不为空是这样的:

   [
    ["tokyo",34],
    ["rome",24],
    ["paris",54]
   ]

使用这个:

   citta_vicine.sort(function(a, b) {
     return a - b;
   }); 

我将得到这个输出:

   [        
    ["rome",24],
    ["tokyo",34],
    ["paris",54]
   ]

我希望你能帮助我,对不起我的英语。

您可能希望在第二个数组元素之后排序:

 citta_vicine.sort(function(a, b) {
   return a[1] - b[1];
 }); 

由于城市数据不是动态变化的,并且您需要经常计算距离/最近邻,因此使用地理空间索引(KD-Tree、R-Tree 等)会很有意义。

这是一个使用 geokdbush 的示例实现,它基于使用 KD-Tree 实现的静态空间索引。它考虑了地球曲率和日期变更线。

const kdbush = require('kdbush');
const geokdbush = require('geokdbush');

// I've stored the data points as objects to make the values unambiguous
const cities = [
  { name: "Abano Terme (PD)", latitude: 45.3594, longitude: 11.7894 },
  { name: "Abbadia Cerreto (LO)", latitude: 45.3122, longitude: 9.5928 },
  { name: "Abbadia Lariana (LC)", latitude: 45.8992, longitude: 9.3336 },
  { name: "Abbadia San Salvatore (SI)", latitude: 42.8800, longitude: 11.6775 },
  { name: "Abbasanta (OR)", latitude: 40.1250, longitude: 8.8200 }
];

// Create the index over city data ONCE
const index = kdbush(cities, ({ longitude }) => longitude, ({ latitude }) => latitude);

// Get the nearest neighbour in a radius of 50km for a point with latitude 43.7051 and longitude 11.4363
const nearest = geokdbush.around(index, 11.4363, 43.7051, 1, 50);

再次记住,kdbush 是一个静态索引,无法更改(您无法从中添加或删除城市)。如果您需要在初始化后更改城市数据,这取决于您更改的频率,使用索引可能成本太高。

To get nearest city...

如果您只对最近的城市感兴趣,则无需对整个列表进行排序。这是您在一行代码中获得的第一个性能提升!

// Unneeded sort:
const closest = cityDistancePairs.sort((a, b) => a[1] - b[2])[0];

// Only iterates once instead:
const closestAlt = cityDistancePairs.reduce(
  (closest, current) => current[1] < closest[1] ? current : closest
);

要进一步优化,您需要对代码的哪些部分进行基准测试需要最长 运行。一些想法:

  • 在计算精确值之前快速检查 lat/lon 差异。如果坐标之间的距离超过一定的增量,您就已经知道它们超出了范围。
  • 通过实现记忆模式来缓存计算的距离,以确保在具有不同限制 (50 -> 100) 的第二次传递时,您不会重新计算距离

但是,我无法想象 8000 次距离计算的循环是真正的性能消耗...我猜测 解析 javascript[=27 的 300kb =] 才是真正的瓶颈。你如何初始化城市数组?

确保将数据集精简为仅包含您实际使用的属性和值。如果您知道您只会使用名称和 lat/lon,则可以预处理数据以仅包含这些。这可能会使它比 300kb 小得多并且更易于使用。

颠覆你的计算

步骤 #1 计算所有位置的距离。

步骤 #2 按距离值对结果进行排序

步骤 #3 查找位置,重复此步骤至少找到一条记录。

勾选DEMO,计算一个有7320条记录的列表用了~17.22119140625ms

const citites = [
  [`Abano Terme (PD)`, 45.3594, 11.7894],
  [`Abbadia Cerreto (LO)`, 45.3122, 9.5928],
  [`Abbadia Lariana (LC)`, 45.8992, 9.3336],
  [`Abbadia San Salvatore (SI)`, 42.8800, 11.6775],
  [`Abbasanta (OR)`, 40.1250, 8.8200]
]

function distance(lat, long) {
  const R = 6372.795477598
  const PI = Math.PI / 180
  return cities
    .map(city => {
      const laA = city[1] * PI
      const laB = lat * PI
      const loA = city[2] * PI
      const loB = long * PI
      const dist = R * Math.acos(
        Math.sin(laA) * Math.sin(laB) +
        Math.cos(laA) * Math.cos(laB) * Math.cos(loA - loB)
      ) || 0
      return { dist, city }
    })
    .sort((a, b) => a.dist - b.dist)
}


function nearest(dist, lat, long) {
  const locations = distance(lat, long)
  function find(delta) {
    const result = []
    for (let location of locations) {
      if (location.dist > delta) break
      result.push(location.city)
    }
    return result.length > 0
      ? result
      : find(delta + 50)
  }
  return find(dist)
}

const result = nearest(50, 41.89595563, 12.48325842)