A detailed vintage map showcasing global geography with an old paper texture.

HOW TO CREATE WMS WITH FASTAPI

Learn how to create a lightweight Web Map Service (WMS) using FastAPI. This step-by-step guide covers setting up WMS APIs, rendering map tiles, handling spatial data, and implementing OGC standards with modern Python libraries.

Creating a API server to support WMS

File Structure

app/
|- main.py
|- wms/
|  |- __init__.py
|  |- router.py
|  |- renderer.py

Prepare a requirements.txt

fastapi[standard]==0.115.6

# Geospatial processing libraries
Pillow==10.0.1
pyproj==3.6.0

Install dependencies

pip install -r requirements.txt

router.py
Create a FastAPI router for WMS APIs.

from fastapi import APIRouter, Query, HTTPException
from starlette.responses import Response
from .renderer import render_map

router = APIRouter()

@router.get("/wms", summary="Web Map Service")
async def wms(
    service: str = Query(..., description="Service type (WMS)"),
    request: str = Query(..., description="Request type (GetMap)"),
    layers: str = Query(..., description="Comma-separated layer names"),
    bbox: str = Query(..., description="Bounding box (minx,miny,maxx,maxy)"),
    width: int = Query(..., description="Map width in pixels"),
    height: int = Query(..., description="Map height in pixels"),
    crs: str = Query("EPSG:4326", description="Coordinate Reference System"),
    format: str = Query("image/png", description="Output format"),
):
    # Validate the service and request types
    if service.upper() != "WMS" or request.upper() != "GETMAP":
        raise HTTPException(status_code=400, detail="Invalid service or request type")
    
    # Parse the bounding box
    try:
        minx, miny, maxx, maxy = map(float, bbox.split(","))
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid bounding box format")
    
    # Render the map
    image = render_map(layers, (minx, miny, maxx, maxy), width, height, crs)
    
    # Return the map image
    if format.lower() == "image/png":
        return Response(image, media_type="image/png")
    else:
        raise HTTPException(status_code=400, detail="Unsupported format")

renderer.py
Use Pillow for rendering

from PIL import Image, ImageDraw
import io

def render_map(layers: str, bbox: tuple, width: int, height: int, crs: str) -> bytes:
    """
    Render a map image for given parameters.
    """
    minx, miny, maxx, maxy = bbox
    layer_list = layers.split(",")
    
    # Create a blank image
    image = Image.new("RGBA", (width, height), "white")
    draw = ImageDraw.Draw(image)
    
    # Example: Draw bounding box (you can replace this with real data rendering)
    draw.rectangle([(0, 0), (width, height)], outline="black")
    draw.text((10, 10), f"Layers: {', '.join(layer_list)}", fill="black")
    draw.text((10, 30), f"BBOX: {bbox}", fill="black")
    draw.text((10, 50), f"CRS: {crs}", fill="black")
    
    # Convert image to bytes
    img_bytes = io.BytesIO()
    image.save(img_bytes, format="PNG")
    return img_bytes.getvalue()

main.py
Register the WMS router in the main FastAPI app.

from fastapi import FastAPI
from wms.router import router as wms_router

app = FastAPI(title="FastAPI WMS Server")

app.include_router(wms_router, prefix="/api")

# Test root endpoint
@app.get("/")
async def root():
    return {"message": "Welcome to FastAPI WMS Server"}

Testing

Run the application

fastapi dev

Access the WMS endpoint in your browser

http://127.0.0.1:8000/api/wms?service=WMS&request=GetMap&layers=test_layer&bbox=-180,-90,180,90&width=800&height=400&crs=EPSG:4326&format=image/png

The image shows at your browser.

References

git: /https://github.com/weeshin/pygeo-server

Leave a Comment

Your email address will not be published. Required fields are marked *