Overview
This guide covers advanced usage patterns for Lynker Spatial tiles, including direct HTTP access, programmatic bulk downloads, and custom application integration.
1. Direct HTTP Access with curl
Basic Tile Request
# Get a single tile
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
"https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11" \
-o tile.pbf
# Check response headers
curl -I -H "Authorization: Bearer YOUR_JWT_TOKEN" \
"https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11"
Downloading Multiple Tiles
#!/bin/bash
# Configuration
TOKEN="YOUR_JWT_TOKEN"
SOURCE="padus"
ZOOM=5
OUTPUT_DIR="./tiles"
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Download tiles for specific X range
for x in {0..31}; do
for y in {0..31}; do
URL="https://tiles.lynker-spatial.com/api/tiles/$SOURCE/$ZOOM/$x/$y"
OUTPUT="$OUTPUT_DIR/${ZOOM}_${x}_${y}.pbf"
echo "Downloading $x/$y..."
curl -s -H "Authorization: Bearer $TOKEN" \
"$URL" -o "$OUTPUT"
# Check if file is empty (error response)
if [ ! -s "$OUTPUT" ]; then
echo " Error downloading $ZOOM/$x/$y"
rm "$OUTPUT"
fi
done
done
echo "Download complete. Files saved to $OUTPUT_DIR"
Batch Download with Parallel Requests
#!/bin/bash
# Using GNU parallel for 4 concurrent downloads
TOKEN="YOUR_JWT_TOKEN"
SOURCE="padus"
ZOOM=6
# Generate list of tiles
get_tiles() {
for x in {0..63}; do
for y in {0..63}; do
echo "$x $y"
done
done
}
# Download function
download_tile() {
local x=$1
local y=$2
local zoom=$ZOOM
local source=$SOURCE
local token=$TOKEN
URL="https://tiles.lynker-spatial.com/api/tiles/$source/$zoom/$x/$y"
OUTPUT="./tiles/${zoom}_${x}_${y}.pbf"
curl -s -H "Authorization: Bearer $token" \
"$URL" -o "$OUTPUT" 2>/dev/null
echo "Downloaded $zoom/$x/$y"
}
export -f download_tile
export TOKEN SOURCE ZOOM
# Run with 4 parallel jobs
get_tiles | parallel --jobs 4 'download_tile {1} {2}'
2. Bulk Download API (Python)
Simple Python Downloader
#!/usr/bin/env python3
import requests
import os
import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
class LynkerTileDownloader:
def __init__(self, token, base_url="https://tiles.lynker-spatial.com"):
self.token = token
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {token}"
})
def download_tile(self, source, z, x, y, output_dir):
"""Download a single tile"""
url = f"{self.base_url}/api/tiles/{source}/{z}/{x}/{y}"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
# Create directory structure
tile_dir = Path(output_dir) / source / str(z) / str(x)
tile_dir.mkdir(parents=True, exist_ok=True)
# Save tile
tile_path = tile_dir / f"{y}.pbf"
with open(tile_path, 'wb') as f:
f.write(response.content)
return True, f"Downloaded {source}/{z}/{x}/{y}"
except Exception as e:
return False, f"Error downloading {source}/{z}/{x}/{y}: {str(e)}"
def download_region(self, source, z, bbox, output_dir, max_workers=4):
"""
Download all tiles in a region
bbox: [min_x, min_y, max_x, max_y]
"""
min_x, min_y, max_x, max_y = bbox
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for x in range(min_x, max_x + 1):
for y in range(min_y, max_y + 1):
future = executor.submit(
self.download_tile,
source, z, x, y, output_dir
)
futures.append(future)
# Process results
success_count = 0
for future in as_completed(futures):
success, message = future.result()
print(message)
if success:
success_count += 1
return success_count
# Usage
if __name__ == "__main__":
import os
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("LYNKER_TOKEN")
downloader = LynkerTileDownloader(token)
# Download zoom 5 tiles in a specific region
bbox = [8, 10, 12, 14] # x_min, y_min, x_max, y_max
success = downloader.download_region(
source="padus",
z=5,
bbox=bbox,
output_dir="./tiles",
max_workers=4
)
print(f"\\nDownloaded {success} tiles successfully")
3. REST API Queries
List Available Sources
# List available tile sources
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://tiles.lynker-spatial.com/catalog"
# Response example:
# {
# "sources": [
# {
# "id": "padus",
# "name": "Protected Areas",
# "description": "USA Protected Areas Database"
# },
# ...
# ]
# }
Get Metadata (TileJSON)
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://tiles.lynker-spatial.com/catalog" | jq '.'
# Returns TileJSON format with:
# - name, description
# - min_zoom, max_zoom
# - bounds (geographic extent)
# - vector_layers (feature types)
Feature Querying
# Query features at a specific tile
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11" --output tile.pbf
# Returns GeoJSON FeatureCollection with all features in that tile
4. Command-Line GIS Tools
Using GDAL/QGIS OGR Tools
# Convert PMTiles to GeoPackage
ogr2ogr -f GPKG output.gpkg \
/vsicurl/https://tiles.lynker-spatial.com/api/tiles/padus/0/0/0
# Reproject to different coordinate system
ogr2ogr -f GPKG output_proj.gpkg \
-t_srs EPSG:3857 output.gpkg
Mapbox Vector Tile Tool (mbutil)
# Download MBTiles database
mbutil --export padus_tiles.mbtiles ./tiles
# Query specific zoom level
mbutil --export --zoom-levels 5-8 padus_tiles.mbtiles ./tiles_z5-8
5. Custom Web Application
Express.js Server with Tile Proxy
const express = require('express');
const axios = require('axios');
const rateLimit = require('express-rate-limit');
const NodeCache = require('node-cache');
const app = express();
const cache = new NodeCache({ stdTTL: 3600 });
// Rate limiting (example for a local proxy)
// Note: This Express example shows one way to protect a self-hosted proxy
// by limiting requests per IP. The production Lynker deployment enforces
// monthly data quotas per token (5 GB) and relies on CDN/API protections
// (CloudFront, API Gateway, WAF) for request-level throttling; adjust
// per-deployment as needed.
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100 // limit each IP to 100 requests per windowMs (example)
});
app.use('/api/tiles', limiter);
// Proxy endpoint
app.get('/api/tiles/:source/:z/:x/:y', async (req, res) => {
const { source, z, x, y } = req.params;
const token = process.env.LYNKER_TOKEN;
const cacheKey = `${source}/${z}/${x}/${y}`;
// Check cache
let data = cache.get(cacheKey);
if (data) {
res.set('X-Cache', 'HIT');
res.set('Content-Type', 'application/x-protobuf');
return res.send(data);
}
try {
// Fetch from Lynker
const response = await axios.get(
`https://tiles.lynker-spatial.com/api/tiles/${source}/${z}/${x}/${y}`,
{
headers: { 'Authorization': `Bearer ${token}` },
responseType: 'arraybuffer',
timeout: 10000
}
);
// Cache result
cache.set(cacheKey, response.data);
res.set('X-Cache', 'MISS');
res.set('Content-Type', 'application/x-protobuf');
res.set('Content-Encoding', 'gzip');
res.send(response.data);
} catch (error) {
const status = error.response?.status || 500;
res.status(status).json({
error: 'Failed to fetch tile',
message: error.message,
source,
tile: `${z}/${x}/${y}`
});
}
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
app.listen(3000, () => {
console.log('Proxy server running on http://localhost:3000');
});
6. Data Processing & Analysis
Converting PMBTiles to Vector Format
#!/usr/bin/env python3
# Convert vector tiles to GeoJSON
import json
from mapbox_vector_tile import decode
def tiles_to_geojson(tiles_dir, output_file):
"""Convert tiles to GeoJSON"""
features = []
for root, dirs, files in os.walk(tiles_dir):
for file in files:
if not file.endswith('.pbf'):
continue
path = os.path.join(root, file)
with open(path, 'rb') as f:
tile_data = f.read()
# Decode tile
decoded = decode(tile_data)
# Extract features
for layer_name, layer in decoded.items():
for feature in layer['features']:
features.append({
'type': 'Feature',
'geometry': feature['geometry'],
'properties': feature['properties'],
'layer': layer_name
})
# Write GeoJSON
with open(output_file, 'w') as f:
json.dump({
'type': 'FeatureCollection',
'features': features
}, f)
print(f"Wrote {len(features)} features to {output_file}")
# Usage
tiles_to_geojson('./tiles', 'output.geojson')
7. Performance Optimization
Caching Strategy
- Browser Cache: Set appropriate HTTP headers in proxy
- Server Cache: Cache tiles in memory or disk for 1-24 hours
- Database Cache: Store frequently accessed tiles in database
- CDN Cache: Front proxy with CloudFront or similar
Proxy Caching Headers
// In Express.js proxy
app.get('/api/tiles/:source/:z/:x/:y.pbf', async (req, res) => {
// ... fetch logic ...
// Set cache headers
res.set('Cache-Control', 'public, max-age=86400'); // 24 hours
res.set('ETag', `"${source}-${z}-${x}-${y}"`);
res.set('Expires', new Date(Date.now() + 86400000).toUTCString());
res.send(data);
});
8. Troubleshooting Advanced Scenarios
Large Tile Downloads Timing Out
# Increase request timeout
curl --max-time 300 \
-H "Authorization: Bearer YOUR_TOKEN" \
"https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11" \
-o tile.pbf
# Python:
requests.get(url, timeout=300)
Handling Authentication Errors
#!/bin/bash
check_auth() {
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $1" \
"https://tiles.lynker-spatial.com/catalog")
status=$(echo "$response" | tail -n1)
if [ "$status" -eq 401 ]; then
echo "ERROR: Invalid or expired token"
exit 1
elif [ "$status" -eq 403 ]; then
echo "ERROR: Insufficient permissions"
exit 1
elif [ "$status" -eq 200 ]; then
echo "Authentication successful"
return 0
else
echo "Unexpected status: $status"
exit 1
fi
}
check_auth "YOUR_JWT_TOKEN"
9. Integration with Existing Systems
WMS/WMTS Server Setup
For legacy systems requiring WMS/WMTS, use MapServer or GeoServer to wrap the tiles:
# Example WMS request to local server wrapping Lynker tiles
curl "http://local-wms:8080/wms?
SERVICE=WMS&
VERSION=1.1.1&
REQUEST=GetMap&
LAYERS=protected_areas&
BBOX=-125,25,-66,49&
WIDTH=800&
HEIGHT=600&
SRS=EPSG:4326&
FORMAT=image/png"
Resources