Add service for Photon requests
This commit is contained in:
102
app/services/photon.js
Normal file
102
app/services/photon.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
|
||||||
|
export default class PhotonService extends Service {
|
||||||
|
baseUrl = 'https://photon.komoot.io/api/';
|
||||||
|
|
||||||
|
async search(query, lat, lon, limit = 10) {
|
||||||
|
if (!query || query.length < 2) return [];
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
limit: String(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lat && lon) {
|
||||||
|
params.append('lat', String(lat));
|
||||||
|
params.append('lon', String(lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}?${params.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.fetchWithRetry(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Photon request failed with status ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.features) return [];
|
||||||
|
|
||||||
|
return data.features.map((f) => this.normalizeFeature(f));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Photon search error:', e);
|
||||||
|
// Return empty array on error so UI doesn't break
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeFeature(feature) {
|
||||||
|
const props = feature.properties || {};
|
||||||
|
const geom = feature.geometry || {};
|
||||||
|
const coords = geom.coordinates || [];
|
||||||
|
|
||||||
|
// Photon returns [lon, lat] for Point geometries
|
||||||
|
const lon = coords[0];
|
||||||
|
const lat = coords[1];
|
||||||
|
|
||||||
|
// Construct a description from address fields
|
||||||
|
// Priority: name -> street -> city -> state -> country
|
||||||
|
const addressParts = [];
|
||||||
|
if (props.street)
|
||||||
|
addressParts.push(
|
||||||
|
props.housenumber
|
||||||
|
? `${props.street} ${props.housenumber}`
|
||||||
|
: props.street
|
||||||
|
);
|
||||||
|
if (props.city && props.city !== props.name) addressParts.push(props.city);
|
||||||
|
if (props.state && props.state !== props.city)
|
||||||
|
addressParts.push(props.state);
|
||||||
|
if (props.country) addressParts.push(props.country);
|
||||||
|
|
||||||
|
const description = addressParts.join(', ');
|
||||||
|
const title = props.name || description || 'Unknown Place';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
osmId: props.osm_id,
|
||||||
|
osmType: props.osm_type, // 'N', 'W', 'R'
|
||||||
|
osmTags: props, // Keep all properties as tags for now
|
||||||
|
description: props.name ? description : addressParts.slice(1).join(', '),
|
||||||
|
source: 'photon',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchWithRetry(url, options = {}, retries = 3) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line warp-drive/no-external-request-patterns
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
|
||||||
|
// Retry on 5xx errors or 429 Too Many Requests
|
||||||
|
if (!res.ok && retries > 0 && [502, 503, 504, 429].includes(res.status)) {
|
||||||
|
console.warn(
|
||||||
|
`Photon request failed with ${res.status}. Retrying... (${retries} left)`
|
||||||
|
);
|
||||||
|
// Exponential backoff or fixed delay? Let's do 1s fixed delay for simplicity
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
return this.fetchWithRetry(url, options, retries - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
// Retry on network errors (fetch throws) except AbortError
|
||||||
|
if (retries > 0 && e.name !== 'AbortError') {
|
||||||
|
console.debug(`Retrying Photon request... (${retries} left)`, e);
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
return this.fetchWithRetry(url, options, retries - 1);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
tests/unit/services/photon-test.js
Normal file
90
tests/unit/services/photon-test.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'marco/tests/helpers';
|
||||||
|
|
||||||
|
module('Unit | Service | photon', function (hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
test('it exists', function (assert) {
|
||||||
|
let service = this.owner.lookup('service:photon');
|
||||||
|
assert.ok(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search handles successful response', async function (assert) {
|
||||||
|
let service = this.owner.lookup('service:photon');
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = async () => {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
name: 'Test Place',
|
||||||
|
osm_id: 123,
|
||||||
|
osm_type: 'N',
|
||||||
|
city: 'Test City',
|
||||||
|
country: 'Test Country',
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
coordinates: [13.4, 52.5], // lon, lat
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await service.search('Test', 52.5, 13.4);
|
||||||
|
assert.strictEqual(results.length, 1);
|
||||||
|
assert.strictEqual(results[0].title, 'Test Place');
|
||||||
|
assert.strictEqual(results[0].lat, 52.5);
|
||||||
|
assert.strictEqual(results[0].lon, 13.4);
|
||||||
|
assert.strictEqual(results[0].description, 'Test City, Test Country');
|
||||||
|
} finally {
|
||||||
|
window.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search handles empty response', async function (assert) {
|
||||||
|
let service = this.owner.lookup('service:photon');
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = async () => {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ features: [] }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await service.search('Nonexistent', 52.5, 13.4);
|
||||||
|
assert.strictEqual(results.length, 0);
|
||||||
|
} finally {
|
||||||
|
window.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeFeature handles missing properties', function (assert) {
|
||||||
|
let service = this.owner.lookup('service:photon');
|
||||||
|
|
||||||
|
const feature = {
|
||||||
|
properties: {
|
||||||
|
street: 'Main St',
|
||||||
|
housenumber: '123',
|
||||||
|
city: 'Metropolis',
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
coordinates: [10, 20],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.normalizeFeature(feature);
|
||||||
|
assert.strictEqual(result.title, 'Main St 123, Metropolis'); // Fallback to address description
|
||||||
|
assert.strictEqual(result.lat, 20);
|
||||||
|
assert.strictEqual(result.lon, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user