Files
to-hen/osm-restaurants/osm-restaurants.ts

127 lines
3.5 KiB
TypeScript
Executable File

#!/usr/bin/env -S deno run --unstable --allow-env --allow-read --allow-write --allow-net
import { createHash } from "https://deno.land/std@0.165.0/node/crypto.ts";
import { homedir } from "https://deno.land/std@0.165.0/node/os.ts";
import { join } from "https://deno.land/std@0.165.0/path/mod.ts";
import {
readFileSync,
mkdirSync,
statSync,
writeFileSync,
existsSync,
} from "https://deno.land/std@0.165.0/node/fs.ts";
import openingHours from "npm:opening_hours@^3.8.0";
import yargs from "https://deno.land/x/yargs/deno.ts";
import { Arguments } from "https://deno.land/x/yargs/deno-types.ts";
const CACHE_AGE = 12 * 60 * 60 * 1000;
interface OsmNode {
lat: number;
lon: number;
tags: { [tag: string]: string };
}
const md5 = (text: string) => createHash("md5").update(text).digest("hex");
const getCachePath = (query: string) => {
const cacheDirectory =
Deno.env.get("CACHE_DIR") ||
join(homedir() || ".", ".cache", "osm-restaurants");
if (!existsSync(cacheDirectory)) mkdirSync(cacheDirectory);
return join(cacheDirectory, md5(query) + ".json");
};
const randomElement = (array: any[]): any =>
array[Math.floor(array.length * Math.random())];
const argv = yargs(Deno.args)
.option("checkOpen", {
alias: "o",
default: true,
type: "boolean",
description: "Only show restaurants that are open",
})
.option("radius", {
alias: "r",
description: "Radius around the location (metres)",
type: "number",
default: 500,
})
.option("latitude", {
description: "The latitude coordinates to search around",
type: "number",
demandOption: true,
})
.option("longitude", {
description: "The latitude coordinates to search around",
type: "number",
demandOption: true,
})
.option("endpoint", {
description: "The Overpass API endpoint",
type: "string",
default: "http://overpass-api.de/api/interpreter",
})
.option("country", {
description: "The country you are in (two letter code)",
type: "string",
default: "de",
})
.help()
.alias("help", "h").argv;
const overpassQuery = `
[out:json];
(
node(around:${argv.radius},${argv.latitude},${argv.longitude})[amenity=fast_food];
node(around:${argv.radius},${argv.latitude},${argv.longitude})[amenity=restaurant];
);
out;
`;
const restaurantOpen = (restaurant: OsmNode) =>
"opening_hours" in restaurant.tags
? new openingHours(restaurant.tags.opening_hours, {
lat: restaurant.lat,
lon: restaurant.lon,
address: {
country_code:
"addr:country" in restaurant.tags
? restaurant.tags["addr:country"].toLowerCase()
: argv.country,
},
}).getState()
: true;
const withRestaurants = (callback: (restaurants: OsmNode[]) => any) => {
const cachePath = getCachePath(overpassQuery);
if (
existsSync(cachePath) &&
+new Date() - statSync(cachePath).mtime < CACHE_AGE
)
callback(JSON.parse(readFileSync(cachePath)));
else {
console.log("fetching");
return fetch(argv.endpoint, {
method: "POST",
body: overpassQuery,
})
.then((response) => response.json())
.then((responseJson) => {
const restaurants = responseJson.elements;
writeFileSync(cachePath, JSON.stringify(restaurants));
callback(restaurants);
})
.catch(console.error);
}
};
withRestaurants((restaurants) => {
const randomOpenRestaurant = randomElement(
argv.checkOpen ? restaurants.filter(restaurantOpen) : restaurants
);
console.log(JSON.stringify(randomOpenRestaurant));
});