Geospatial Caching
I was wondering how applications cache GIS data. I learned that there’s a Mapbox Tile Service (MTS) that handles the generation, storage, and delivery of map tiles to an client application. You create a tile set using your own data, like a GeoJSON, shapefile, etc., and then your application can either request those tiles from MTS directly, or cached/proxied through your server. In the latter case, the client application would make a request to your server, and you would check your cache for a tile. If that tile was not found, you’d query the MTS system for the tile, and then deliver it to the client application.
Here is an example FastAPI backend that serves as a reverse proxy to MapBox.
import os
import json
import redis
import requests
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import Response
MAPBOX_ACCESS_TOKEN = os.environ.get("MAPBOX_ACCESS_TOKEN")
if not MAPBOX_ACCESS_TOKEN:
raise ValueError("MAPBOX_ACCESS_TOKEN environment variable not set")
REDIS_HOST = os.environ.get("REDIS_HOST", "localhost")
REDIS_PORT = int(os.environ.get("REDIS_PORT", 6379))
REDIS_DB = int(os.environ.get("REDIS_DB", 0))
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB)
app = FastAPI()
@app.get("/tiles/{z}/{x}/{y}.{format}")
async def get_tile(z: int, x: int, y: int, format: str, tileset_id: str = Query("mapbox.mapbox-streets-v8")):
tile_key = f"{tileset_id}/{z}/{x}/{y}.{format}"
cached_tile = redis_client.get(tile_key)
if cached_tile:
return Response(content=cached_tile, media_type=f"image/{format}" if format in ["png", "jpg", "jpeg"] else "application/x-protobuf")
else:
try:
url = f"https://api.mapbox.com/v4/{tileset_id}/{z}/{x}/{y}.{format}?access_token={MAPBOX_ACCESS_TOKEN}"
response = requests.get(url, stream=True)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
tile_content = response.content
redis_client.setex(tile_key, 3600, tile_content) # Cache for 1 hour
return Response(content=tile_content, media_type=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
raise HTTPException(status_code=500, detail=str(e))
And this is an example of a JS frontend component that serves a MapBoxGL component.
import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN'; // For base map only
const Map = () => {
const mapContainer = useRef(null);
const [map, setMap] = useState(null);
useEffect(() => {
const initializeMap = () => {
const map = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12', // Or another base map style
center: [-77.032, 38.913],
zoom: 9
});
map.on('load', () => {
map.addSource('my-tiles', {
'type': 'raster', // or vector for .pbf
'tiles': [
'http://localhost:8000/tiles/{z}/{x}/{y}.png?tileset_id=mapbox.satellite', // Use your backend URL
],
'tileSize': 256
});
map.addLayer({
'id': 'my-tiles-layer',
'type': 'raster',
'source': 'my-tiles',
'minzoom': 0,
'maxzoom': 22
});
setMap(map);
});
};
if (!map) initializeMap();
}, [map]);
return <div ref={mapContainer} />;
};
export default Map;