我正在寻找用于处理地理空间数据的 node.js 库。我在数据库中有大量地理标记数据。
所需的功能包括:
- 地理编码和反向地理编码
- 查找附近的项目
- 查找地质范围内的项目
就像一个相当于 RoR 中 GeoCoder gem 的节点。
在尝试自己回答时发现了这个问题,我会更新这个。
上面推荐的地理编码器库最后一次更新是在 2014 年,似乎没有处理 Promises。node-geocoder正在被积极维护,如果你使用 Babel/ES7,它基于 Promise 的 API 可以让你做这样的事情:
const NodeGeocoder = require('node-geocoder');
let geocoder = NodeGeocoder()
let assert = require('assert');
describe('geocoder#geocode()', () => {
it('geocodes things', async () => {
let result = await geocoder.geocode("Ottawa, Canada")
assert.equal(45.4215296, result[0].latitude)
})
})
我确实发现 node-geocoder 给出了它从谷歌/任何人那里得到的结果的一个子集,其中 Ruby 的地理编码器项目似乎给出了整个响应。
//result returned from geocoder.geocode:
[ { formattedAddress: 'Ottawa, ON, Canada',
latitude: 45.4215296,
longitude: -75.69719309999999,
extra:
{ googlePlaceId: 'ChIJrxNRX7IFzkwR7RXdMeFRaoo',
confidence: 0.5,
premise: null,
subpremise: null,
neighborhood: null,
establishment: null },
administrativeLevels:
{ level2long: 'Ottawa Division',
level2short: 'Ottawa Division',
level1long: 'Ontario',
level1short: 'ON' },
city: 'Ottawa',
country: 'Canada',
countryCode: 'CA',
provider: 'google' } ]
根据它的 lat/lng 查找数据将由数据库负责。我一直在使用ArangoDB,它们的功能WITHIN_RECTANGLE
是对map.getBounds()
Leaflet / Mapbox -gl-js的完美补充。这是一个例子:
export async function locationsWithinBounds(swLat, swLng, neLat, neLng) {
let query = aqlQuery`
RETURN WITHIN_RECTANGLE(vertices, ${swLat}, ${swLng}, ${neLat}, ${neLng})
`
let results = await db.query(query)
let allResults = await results.all()
return allResults[0]
}
如果您探索他们的其他地理功能,您会发现类似NEAR
和IS_IN_POLYGON
。如果您需要在浏览器中使用地理功能,您应该查看Turf.js。
对于 GeoCoding 和反向部分,您可以使用https://github.com/wyattdanger/geocoder
npm install geocoder
对于查找部分,最好的方法是使用您的数据库系统,如果它支持地理操作。
如果没有,也许将您的数据库复制到另一个数据库中(使用地理操作)?如果您需要处理千兆字节的数据,不是很容易......将所有内容加载到内存中,数据库就是为此而存在的:)。
根据您的应用程序是什么,如果您要索引的东西不是来自直接的 Geocoding / rev-Geocoding API,那么某种形式的 Geohashing 可能会有所帮助。
一种 Geohash 是来自 Google 的 S2,它可以使用 64 位整数以小到指尖到地球表面的精度来索引区域。
这是一个在检索时使用 O(1) 运行时查找是否存在与Node S2 库边界的示例。在这种情况下,它会检查是否有人在布鲁克林的威廉斯堡:
const s2 = require('@radarlabs/s2');
const williamsburgLongLats = [
[-73.95841598510742, 40.72423412682422],
[-73.96957397460938, 40.71226430831242],
[-73.9683723449707, 40.70497727808752],
[-73.96184921264648, 40.69951148213175],
[-73.95923137664795, 40.70852329864894],
[-73.94775152206421, 40.70391994183744],
[-73.94163608551025, 40.71145106322093],
[-73.94335269927979, 40.71834706657437],
[-73.9469575881958, 40.719778223045275],
[-73.94970417022705, 40.722217623379684],
[-73.95056247711182, 40.721892375167045],
[-73.95193576812744, 40.72335597960796],
[-73.95425319671631, 40.72312830991985],
];
const lls = williamsburgLongLats.map((lnglat) => {
const [lng, lat] = lnglat;
return new s2.LatLng(lat, lng);
});
const s2Level = 14; // ~0.32 km^2
const neighborhoodS2Cells =
new Set(s2.RegionCoverer.getCoveringTokens(
lls,
{ min: s2Level, max: s2Level }
));
const user1LongLat = [-73.95429611206055, 40.71369559554873]; // in williamsburg
const user2LongLat = [-73.9266586303711, 40.71616774648679]; // not in williamsburg
const user1S2 = new s2.CellId(new s2.LatLng(user1LongLat[1], user1LongLat[0])).parent(14); // level 14 to ensure 1:1 mapping to cover
const user2S2 = new s2.CellId(new s2.LatLng(user2LongLat[1], user2LongLat[0])).parent(14); // level 14 to ensure 1:1 mapping to cover
console.log(neighborhoodS2Cells.has(user1S2.token())); // true - O(1)
console.log(neighborhoodS2Cells.has(user2S2.token())); // false - O(1)