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