parent
adbb5edb4b
commit
35cf2f4fba
@ -0,0 +1,30 @@ |
|||||||
|
import { createOsmClient } from './osm'; |
||||||
|
|
||||||
|
const sampleAddresses = [ |
||||||
|
{ |
||||||
|
address: 'Hamburger Str. 273A, 38114 Braunschweig, Germany', |
||||||
|
latitude: 52.28, |
||||||
|
longitude: 10.52, |
||||||
|
}, |
||||||
|
{ |
||||||
|
address: 'Carrer de Tapioles, 47, 08004 Barcelona, Spain', |
||||||
|
latitude: 41.37, |
||||||
|
longitude: 2.16, |
||||||
|
}, |
||||||
|
{ |
||||||
|
address: 'Dumlupınar Sk. No:5, 34710 Kadıköy/İstanbul, Türkiye', |
||||||
|
latitude: 40.99, |
||||||
|
longitude: 29.02, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
describe('createOsmClient', () => { |
||||||
|
const client = createOsmClient(); |
||||||
|
for (const sampleAddress of sampleAddresses) { |
||||||
|
it(`resolves ${sampleAddress.address}`, async () => { |
||||||
|
const result = await client.geocode(sampleAddress.address); |
||||||
|
expect(result.latitude).toBeCloseTo(sampleAddress.latitude, 2); |
||||||
|
expect(result.longitude).toBeCloseTo(sampleAddress.longitude, 2); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
@ -0,0 +1,62 @@ |
|||||||
|
import { compact } from 'lodash'; |
||||||
|
import NodeGeocoder from 'node-geocoder'; |
||||||
|
import fetch from 'node-fetch'; |
||||||
|
import PQueue from 'p-queue'; |
||||||
|
import { mean } from '../../utils/math'; |
||||||
|
|
||||||
|
export const createOsmClient = () => { |
||||||
|
const geocoder = NodeGeocoder({ |
||||||
|
provider: 'openstreetmap', |
||||||
|
fetch: (url, options) => { |
||||||
|
return fetch(url, { |
||||||
|
...options, |
||||||
|
headers: { |
||||||
|
...options?.headers, |
||||||
|
'user-agent': |
||||||
|
'test-assignment-parcellab by Inga. This agent is only supposed to send a few requests in two weeks starting on October 23th 2023, and none after that', |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
// Rate limit according to https://operations.osmfoundation.org/policies/nominatim/
|
||||||
|
const queue = new PQueue({ |
||||||
|
interval: 1000, |
||||||
|
intervalCap: 1, |
||||||
|
timeout: 2000, |
||||||
|
throwOnTimeout: true, |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
geocode: async (query: string) => { |
||||||
|
const result = await queue.add(() => geocoder.geocode(query)); |
||||||
|
|
||||||
|
if (!result.length) { |
||||||
|
throw new Error('No results found'); |
||||||
|
} |
||||||
|
|
||||||
|
const meanLatitude = mean( |
||||||
|
compact(result.map(({ latitude }) => latitude)), |
||||||
|
); |
||||||
|
const meanLongitude = mean( |
||||||
|
compact(result.map(({ longitude }) => longitude)), |
||||||
|
); |
||||||
|
if ( |
||||||
|
!result.every( |
||||||
|
({ latitude, longitude }) => |
||||||
|
latitude && |
||||||
|
longitude && |
||||||
|
Math.abs(latitude - meanLatitude) < 0.01 && |
||||||
|
Math.abs(longitude - meanLongitude) < 0.01, |
||||||
|
) |
||||||
|
) { |
||||||
|
throw new Error('Ambiguous address'); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
latitude: meanLatitude, |
||||||
|
longitude: meanLongitude, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,13 @@ |
|||||||
|
import { mean } from './math'; |
||||||
|
|
||||||
|
describe('mean', () => { |
||||||
|
it('computes mean for one value correctly', () => { |
||||||
|
expect(mean([10])).toBe(10); |
||||||
|
}); |
||||||
|
it('computes mean for two value correctly', () => { |
||||||
|
expect(mean([10, 11])).toBe(10.5); |
||||||
|
}); |
||||||
|
it('computes mean for tem values correctly', () => { |
||||||
|
expect(mean([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])).toBe(14.5); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,10 @@ |
|||||||
|
export const mean = (values: number[]) => { |
||||||
|
let currentMean = 0; |
||||||
|
let count = 0; |
||||||
|
for (const value of values) { |
||||||
|
count++; |
||||||
|
currentMean += (value - currentMean) / count; |
||||||
|
} |
||||||
|
|
||||||
|
return currentMean; |
||||||
|
}; |
Loading…
Reference in new issue