diff --git a/apps/blakus-api/src/app/routes/blakus.ts b/apps/blakus-api/src/app/routes/blakus.ts new file mode 100644 index 0000000..f2bc6f5 --- /dev/null +++ b/apps/blakus-api/src/app/routes/blakus.ts @@ -0,0 +1,36 @@ +import { FastifyInstance } from 'fastify'; +import { OverpassService } from '../services/overpass'; +import { OverpassQuerySchema, OverpassQueryType } from '../schemas/overpass'; + +const overpassService = new OverpassService(); + +export default async function (fastify: FastifyInstance) { + + fastify.get<{ Querystring: OverpassQueryType }>('/blakus', { + schema: { + querystring: OverpassQuerySchema, + response: { + 200: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + amenity: { type: 'string' }, + tags: { type: 'object', additionalProperties: true } + } + } + } + } + } + }, async function ({query}) { + const elements = await overpassService.getAmenitiesAroundLocation({ + latlon: { + lat: query.lat, + lon: query.lon, + }, + amenities: ['restaurant', 'cafe', 'bar', 'pub', 'biergarten', 'fast_food', 'food_court', 'ice_cream'] + }); + return elements; + }); +} diff --git a/apps/blakus-api/src/app/schemas/overpass.ts b/apps/blakus-api/src/app/schemas/overpass.ts new file mode 100644 index 0000000..ec78ecf --- /dev/null +++ b/apps/blakus-api/src/app/schemas/overpass.ts @@ -0,0 +1,10 @@ +import { Static, Type } from '@sinclair/typebox' + +export const OverpassQuerySchema = Type.Object({ + lat: Type.Number(), + lon: Type.Number(), + radius: Type.Optional(Type.Number()), + amenities: Type.Optional(Type.Array(Type.String())), +}); + +export type OverpassQueryType = Static; \ No newline at end of file diff --git a/apps/blakus-api/src/app/services/overpass.ts b/apps/blakus-api/src/app/services/overpass.ts new file mode 100644 index 0000000..595ca10 --- /dev/null +++ b/apps/blakus-api/src/app/services/overpass.ts @@ -0,0 +1,89 @@ +import axios, { AxiosInstance } from 'axios'; + +type OverpassResult = { + version: number; + generator: string; + osm3s: Record; + elements: (NodeElement | WayElement | RelationElement)[]; +} + +type OverpassLocation = { + lat: number, + lon: number +} + +type OverpassElement = { + type: string; + id: number; + tags?: { [key: string]: string } +}; + +type NodeElement = OverpassElement & OverpassLocation & { + type: 'node'; + lat: number; + lon: number; +} + +type WayElement = OverpassElement & { + type: 'way'; + nodes: number[]; +} + +type RelationElement = OverpassElement & { + type: 'relation'; + members: { + type: 'node' | 'way' | 'relation'; + ref: number; + role: string; + }[]; +} + +function opq(strings, ...expressions) { + let query = '[out:json];\n'; + + for (let i = 0; i < strings.length; i++) { + query += strings[i]; + if (i < expressions.length) query += expressions[i]; + } + + query += '\nout body;\n>;\nout skel qt;'; + + return query; +} + +export class OverpassService { + private axios: AxiosInstance; + + constructor(baseURL = 'https://overpass-api.de/api/') { + this.axios = axios.create({ baseURL }); + } + + async getAmenitiesAroundLocation({radius = 500, amenities = [], latlon}: {radius?: number, latlon: OverpassLocation, amenities: string[]}) { + + if (!latlon || !latlon.lat || !latlon.lon) throw new Error('Invalid location'); + + const { lat, lon } = latlon; + + const query = opq`node(around:${radius}, ${lat}, ${lon})["amenity"~"${amenities.join('|')}"];`; + + const result = await this.overpassQuery(query); + + return result.elements + .map(({ tags }) => ({ title: tags.name, amenity: tags.amenity, tags})) + .filter(i => amenities.includes(i.amenity)); // query returns some random results. need to look into this + } + + async overpassQuery(query: string): Promise { + return this.getData('interpreter', { data: query }); + } + + async getData(endpoint: string, params: Record): Promise { + try { + const response = await this.axios.get(endpoint, { params }); + return response.data; + } catch (error) { + console.error(`Error fetching data from ${endpoint}:`, error); + throw error; + } + } +} \ No newline at end of file