Adding a Favicon

After more than 15 years of running this blog it is time to add a favicon. Of course I could start Gimp and create one or I could even use some of the Webeditors out there, but I want to do this in Python. It doesn't have to be beautiful. My usecase here is: I want to find my blog in all the open Browser tabs I have.

I decided on using Pillow in a Jupyter Notebook to generate a background color gradient and the letter M.

The code I used for the gradient:

from PIL import Image

def gradient_image(col1: str, col2: str, width: int, angle: int) -> Image:
    base_image = Image.new("RGB", (width, width), col1)
    top_image = Image.new("RGB", (width, width), col2)
    mask = Image.new("L", (width * 2, width * 2))

    # generate a mask to blend the top image over the base image
    data = []
    for y in range(width * 2):
        data.extend(width * [int(90 * (y / width))])

    mask.putdata(data)
    # rotate mask to get a rotated gradient and crop to image size
    mask = mask.rotate(angle).crop((0, 0, width, width))
    base_image.paste(top_image, (0, 0), mask)
    return base_image

This generates two images with col1 and col2 and generates a mask with transparency gradients. Then it rotates the mask (only angles between 0 and 90 are allowed) and applies the mask with the top image on top of the base image. The gradient for the angles 0 to 90 in 10 degree steps look like this:

img1

And the code to add the "M" on top:

from PIL import Image, ImageFont, ImageDraw

# image with blue/yellow gradient
im = gradient_image("lightblue", "yellow", 32, 42)
# the font
font = ImageFont.truetype("/usr/share/fonts/gnu-free/FreeMono.otf", 40)
# add black non transparent M to image
draw = ImageDraw.Draw(im)
draw.text((4, -4), "M", font=font, fill=(0, 0, 0, 255))
# save as icon
im.save("favicon.ico")

The resulting Icon:

img2

is good enough for my usecase. :)

Selfhost OpenRouteService API

After last weeks overpass api selfhost, this week it is selfhosting openrouteservice. They have a ready to modify docker-compose.yml and good documentation for Docker hosting. Maybe a bit too many options and too many choices for someone hosting this the first time.

As last week we will use a Hetzner VPS, I used the same CX32 with 4 cores, 8 GB memory and 80GB disk to start with (and later a CX42 for more memory). Again I used Debian 12 as operating system.

The 8GB of memory are not enough so we create a swapfile with additonal 8GB of memory:

fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
swapon --show

Then I installed Docker Engine.

My modified docker-compose.yml is stripped of everything I don't need: removing car-routing and adding walking. I downloaded baden-wuerttemberg-latest.osm.pdf from Geofabrik and put it into a freshly created ors-docker/files/ folder.

---
services:
  ors-app:
    build:
      context: ./
    container_name: ors-app
    ports:
      - "80:8082"  # Expose the ORS API on port 80
      - "9001:9001"
    image: openrouteservice/openrouteservice:v8.0.0
    volumes:
      - ./ors-docker:/home/ors
    environment:
      REBUILD_GRAPHS: False
      CONTAINER_LOG_LEVEL: INFO
      XMS: 4g  # start RAM assigned to java
      XMX: 10g  # max RAM assigned to java. Rule of Thumb: <PBF-size> * <profiles> * 2
      # Example: 1.5 GB pbf size, two profiles (car and foot-walking)
      # -> 1.5 * 2 * 2 = 6. Set xmx to be AT LEAST `-Xmx6g`
      ADDITIONAL_JAVA_OPTS: ""  # further options you want to pass to the java command
      ors.engine.source_file: /home/ors/files/baden-wuerttemberg-latest.osm.pbf
      ors.engine.profiles.car.enabled: false
      ors.engine.profiles.walking.enabled: true

Then run everything with docker compose up. This took a while. Especially the elevation cache and graph building.

I later tried the pbf for the United States (10GB) which needed more memory (I used a bigger instance with 16Gb of memory and 16GB of swap, and it took more than 7 hours to preprocess.

When the preprocessing is finished, the status page (http://<IP-ADDRESS>/ors/v2/status) for me looked like this:

{
  "languages": [
    "cs", "cs-cz", "de", "de-de", "en", "en-us", "eo", "eo-eo", "es", "es-es", "fr", "fr-fr", "gr", "gr-gr",
    "he", "he-il", "hu", "hu-hu", "id", "id-id", "it", "it-it", "ja", "ja-jp", "nb", "nb-no", "ne", "ne-np",
    "nl", "nl-nl", "pl", "pl-pl", "pt", "pt-pt", "ro", "ro-ro", "ru", "ru-ru", "tr", "tr-tr", "zh", "zh-cn"
  ],
  "engine": {"build_date": "2024-03-21T13:55:54Z", "version": "8.0.0"},
  "profiles": {
    "profile 1": {
      "storages": {
        "HillIndex": {"gh_profile": "pedestrian_ors_fastest"},
        "WayCategory": {"gh_profile": "pedestrian_ors_fastest"},
        "WaySurfaceType": {"gh_profile": "pedestrian_ors_fastest"},
        "TrailDifficulty": {"gh_profile": "pedestrian_ors_fastest"}
      },
      "profiles": "foot-walking",
      "creation_date": "",
      "limits": {
        "maximum_distance": 100000,
        "maximum_waypoints": 50,
        "maximum_distance_dynamic_weights": 100000,
        "maximum_distance_avoid_areas": 100000
      }}},
  "services": ["routing", "isochrones", "matrix", "snap"]
}

Same as last week an example for how to use the API. I only needed the routing for a walking distance and duration estimation. As example we will route from the Empire State building in New York to the center of Central Park. The coordinates are: 40.748448,-73.985630 to 40.782773,-73.965363 (both lat,lon).

import httpx

r = httpx.get(
    "http://<IP-ADDRESS>/ors/v2/directions/foot-walking",
    params={
        # here it is lon,lat
        "start": "-73.985630,40.748448",
        "end": "-73.965363,40.782773",
    },
)
print(r.json()["features"][0]["properties"]["summary"])

This returns {'distance': 4421.3, 'duration': 3183.2}, so 4.4 km and 53 minutes. I compared this to Google Maps Routing and it is in the same ballpark.

Selfhost OverPass API

I don't know how many Overpass Api requests I will need for my current hobby project idea. So I decided to try selfhosting Overpass.

There is a ready to use AWS AMI with the planet inside. But I don't want to use AWS for private projects so I will try to run this on a Hetzner VPS instance. The server I used was a CX32 with 4 cores, 8 GB memory and 80GB disk. As operating system I used Debian 12, but any Linux should work here.

The 8GB of memory are not enough so we create a swapfile with additonal 8GB of memory:

fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
swapon --show

Then I installed Docker Engine.

I converted the Docker command from the Overpass API readme into a Docker Compose file. Here I replaced Monaco with Baden-Württemberg and used the more current pbf version and not the .osm.bz2 one. The initial processing for converting the 4GB pbf into bz2 takes a while, but this should be faster than processing the minutes for 200 days (If that is even possible/happening). Also the whole importing and updating before the first start takes quite a bit of time and this is also the memory consuming part.

The docker-compose.yml I used:

---
services:
  overpass:
    image: wiktorn/overpass-api
    container_name: overpass
    environment:
      OVERPASS_META: no
      OVERPASS_MODE: init
      OVERPASS_PLANET_URL: https://download.geofabrik.de/europe/germany/baden-wuerttemberg-latest.osm.pbf
      OVERPASS_DIFF_URL: https://download.geofabrik.de/europe/germany/baden-wuerttemberg-updates/
      OVERPASS_RULES_LOAD: 10
      OVERPASS_COMPRESSION: gz
      OVERPASS_UPDATE_SLEEP: 3600
      OVERPASS_PLANET_PREPROCESS: 'mv /db/planet.osm.bz2 /db/planet.osm.pbf && osmium cat -o /db/planet.osm.bz2 /db/planet.osm.pbf && rm /db/planet.osm.pbf'
    volumes:
      - ./overpass_db/:/db
    ports:
      - "80:80"

And then run with docker compose up. This will take quite some time and may swap a bit, depending on the size of your planet file and the minutes. When the processing is done, the container will exit and when you restart it again the same way it will be live and happy to serve.

Now the API is running and we use the same query as in my overpass post from last year. Only this time we use our own overpass instance.

import json
# pip install OSMPythonTools
from OSMPythonTools import overpass

ip = "IP_ADDRESS_OF_YOUR_SERVER"
api = overpass.Overpass(endpoint=f"http://{ip}/api/")
# bbox for Stuttgart Mitte (roughly)
bbox_coords = [48.76605, 9.1657, 48.78508, 9.18995]
query = overpass.overpassQueryBuilder(
    bbox=bbox_coords,
    elementType=["node", "way"],
    # all restaurants that have a positive diet:vegan tag
    selector=['"amenity"="restaurant"', '"diet:vegan"~"yes|only"'],
    out="body",
)
result = api.query(query, timeout=60)

for index, item in enumerate(result.toJSON()["elements"]):
    print(index, item["tags"])

Last time 14 vegan restaurants were found in Stuttgart Mitte. This increased by one to 15 restaurants.