Created
August 20, 2025 02:34
-
-
Save luandro/4ebaeb200d0718b807bf4af57beb936b to your computer and use it in GitHub Desktop.
Generate SMP from QGIS XYZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# List all folders except numeric folders | |
options=($(ls -d */ | grep -Ev '^[0-9]+/$' | grep -v 'generated_smp/')) | |
echo "Select a folder:" | |
select selected_folder in "${options[@]}"; do | |
if [[ -n $selected_folder ]]; then | |
# Remove trailing slash | |
selected_folder=${selected_folder%/} | |
# Find highest numbered folder (max zoom) | |
max_zoom=$(ls -d [0-9]*/ 2>/dev/null | sort -V | tail -1 | tr -d '/') | |
if [[ -z $max_zoom ]]; then | |
echo "Error: No numbered folders found" | |
exit 1 | |
fi | |
# Debug: show detected folders | |
echo "Available numbered folders: $(ls -d [0-9]*/ 2>/dev/null | tr -d '/' | tr '\n' ' ')" | |
# Prepare variables | |
current_year=$(date +%Y) | |
folder_name="$selected_folder" | |
# Prompt user for custom name | |
echo -n "Enter a name for the SMP file (or press Enter for default): " | |
read -r custom_name | |
if [[ -n "$custom_name" ]]; then | |
# Use custom name with selected folder and max zoom | |
name="${custom_name}-${folder_name}-${current_year}-z${max_zoom}" | |
else | |
# Use default name pattern | |
name="tiles-${folder_name}-${current_year}-z${max_zoom}" | |
fi | |
tmp_dir="temp_${name}_$(date +%s)" | |
output_dir="generated_smp" | |
echo "Processing folder: $selected_folder" | |
echo "Max zoom level: $max_zoom" | |
echo "Output name: $name" | |
# Create temp directory structure | |
mkdir -p "$tmp_dir/s/0" | |
# Copy all numbered folders to s/0 | |
echo "Copying numbered folders (0-$max_zoom) to s/0..." | |
for i in $(seq 0 $max_zoom); do | |
if [[ -d "$i" ]]; then | |
echo " Copying folder: $i" | |
cp -r "$i" "$tmp_dir/s/0/" | |
else | |
echo " Skipping missing folder: $i" | |
fi | |
done | |
# Merge selected folder content into s/0 | |
echo "Merging $selected_folder content into s/0..." | |
if [[ -d "$selected_folder" ]]; then | |
if ls "$selected_folder"/* > /dev/null 2>&1; then | |
cp -r "$selected_folder"/* "$tmp_dir/s/0/" 2>/dev/null || true | |
echo " Content merged successfully" | |
else | |
echo " Selected folder is empty" | |
fi | |
else | |
echo " Warning: Selected folder does not exist" | |
fi | |
# Process JSON files to create multiple sources | |
echo "Creating style.json with multiple sources from template..." | |
# Function to convert EPSG:3857 to WGS84 | |
convert_bounds() { | |
local extent="$1" | |
# Extract coordinates from EXTENT format: "xmin,xmax,ymin,ymax [EPSG:3857]" | |
local coords=$(echo "$extent" | sed 's/ \[EPSG:3857\]//g') | |
local xmin=$(echo "$coords" | cut -d',' -f1) | |
local xmax=$(echo "$coords" | cut -d',' -f2) | |
local ymin=$(echo "$coords" | cut -d',' -f3) | |
local ymax=$(echo "$coords" | cut -d',' -f4) | |
# Convert from EPSG:3857 to WGS84 (simplified conversion) | |
# For longitude: lon = x / 20037508.34 * 180 | |
# For latitude: lat = atan(sinh(y / 20037508.34 * pi)) * 180 / pi | |
local lon_min=$(echo "scale=8; $xmin / 20037508.34 * 180" | bc -l) | |
local lon_max=$(echo "scale=8; $xmax / 20037508.34 * 180" | bc -l) | |
# Simplified latitude conversion (good enough for bounds) | |
local lat_min=$(echo "scale=8; 2 * a(e($ymin / 20037508.34 * 3.14159265)) * 57.2958 - 90" | bc -l 2>/dev/null || echo "scale=6; $ymin * 0.000000009" | bc) | |
local lat_max=$(echo "scale=8; 2 * a(e($ymax / 20037508.34 * 3.14159265)) * 57.2958 - 90" | bc -l 2>/dev/null || echo "scale=6; $ymax * 0.000000009" | bc) | |
echo "[$lon_min, $lat_min, $lon_max, $lat_max]" | |
} | |
# Function to calculate center from bounds | |
calculate_center() { | |
local extent="$1" | |
local coords=$(echo "$extent" | sed 's/ \[EPSG:3857\]//g') | |
local xmin=$(echo "$coords" | cut -d',' -f1) | |
local xmax=$(echo "$coords" | cut -d',' -f2) | |
local ymin=$(echo "$coords" | cut -d',' -f3) | |
local ymax=$(echo "$coords" | cut -d',' -f4) | |
# Calculate center in Web Mercator | |
local x_center=$(echo "scale=2; ($xmin + $xmax) / 2" | bc -l) | |
local y_center=$(echo "scale=2; ($ymin + $ymax) / 2" | bc -l) | |
# Convert center to WGS84 | |
local lon_center=$(echo "scale=8; $x_center / 20037508.34 * 180" | bc -l) | |
local lat_center=$(echo "scale=8; 2 * a(e($y_center / 20037508.34 * 3.14159265)) * 57.2958 - 90" | bc -l 2>/dev/null || echo "scale=6; $y_center * 0.000000009" | bc) | |
echo "[$lon_center, $lat_center]" | |
} | |
# Initialize variables for style generation | |
declare -A json_data | |
sources_json="" | |
layers_json="" | |
source_folders_json="" | |
global_bounds="[-73.740234375, -7.231698708367133, -69.3896484375, -4.346411275333186]" | |
center_coords="[-71.56494140625, -5.7890549918501595]" | |
highest_zoom=0 | |
highest_zoom_extent="" | |
source_count=0 | |
# Process JSON files from current directory | |
for json_file in *.json; do | |
if [[ -f "$json_file" && "$json_file" != "style.json" && "$json_file" != "style_template.json" ]]; then | |
echo "Processing JSON file: $json_file" | |
# Extract data from JSON | |
minzoom=$(jq -r '.inputs.ZOOM_MIN // 0' "$json_file" 2>/dev/null || echo "0") | |
maxzoom=$(jq -r '.inputs.ZOOM_MAX // 13' "$json_file" 2>/dev/null || echo "13") | |
extent=$(jq -r '.inputs.EXTENT // ""' "$json_file" 2>/dev/null || echo "") | |
# Check if this is the highest zoom level so far | |
if [[ $maxzoom -gt $highest_zoom ]]; then | |
highest_zoom=$maxzoom | |
highest_zoom_extent="$extent" | |
fi | |
# Generate source name from filename | |
source_name="source-$(basename "$json_file" .json)" | |
# Convert bounds if extent is available | |
if [[ -n "$extent" && "$extent" != "null" ]]; then | |
bounds=$(convert_bounds "$extent" 2>/dev/null || echo "$global_bounds") | |
else | |
bounds="$global_bounds" | |
fi | |
# Add comma if not first source | |
if [[ $source_count -gt 0 ]]; then | |
sources_json="$sources_json," | |
source_folders_json="$source_folders_json," | |
fi | |
# Add source to JSON | |
sources_json="$sources_json | |
\"$source_name\": { | |
\"format\": \"jpg\", | |
\"name\": \"$name-$source_name\", | |
\"version\": \"2.0\", | |
\"type\": \"raster\", | |
\"minzoom\": $minzoom, | |
\"maxzoom\": $maxzoom, | |
\"scheme\": \"xyz\", | |
\"bounds\": $bounds, | |
\"center\": [0, 0, 6], | |
\"tiles\": [\"smp://maps.v1/s/0/{z}/{x}/{y}.jpg\"] | |
}" | |
# Add layer to JSON (comma handled separately) | |
if [[ $source_count -eq 0 ]]; then | |
layers_json="$layers_json | |
{ | |
\"id\": \"raster-$source_name\", | |
\"type\": \"raster\", | |
\"source\": \"$source_name\", | |
\"paint\": { | |
\"raster-opacity\": 1 | |
} | |
}" | |
else | |
layers_json="$layers_json, | |
{ | |
\"id\": \"raster-$source_name\", | |
\"type\": \"raster\", | |
\"source\": \"$source_name\", | |
\"paint\": { | |
\"raster-opacity\": 1 | |
} | |
}" | |
fi | |
# Add to source folders metadata | |
source_folders_json="$source_folders_json | |
\"$source_name\": \"0\"" | |
source_count=$((source_count + 1)) | |
fi | |
done | |
# Process JSON files from selected directory if they exist | |
if [[ -d "$selected_folder" ]]; then | |
for json_file in "$selected_folder"/*.json; do | |
if [[ -f "$json_file" ]]; then | |
echo "Processing JSON file from selected folder: $json_file" | |
# Extract data from JSON | |
minzoom=$(jq -r '.inputs.ZOOM_MIN // 0' "$json_file" 2>/dev/null || echo "0") | |
maxzoom=$(jq -r '.inputs.ZOOM_MAX // 13' "$json_file" 2>/dev/null || echo "13") | |
extent=$(jq -r '.inputs.EXTENT // ""' "$json_file" 2>/dev/null || echo "") | |
# Check if this is the highest zoom level so far | |
if [[ $maxzoom -gt $highest_zoom ]]; then | |
highest_zoom=$maxzoom | |
highest_zoom_extent="$extent" | |
fi | |
# Generate source name from filename and folder | |
source_name="source-$selected_folder-$(basename "$json_file" .json)" | |
# Convert bounds if extent is available | |
if [[ -n "$extent" && "$extent" != "null" ]]; then | |
bounds=$(convert_bounds "$extent" 2>/dev/null || echo "$global_bounds") | |
else | |
bounds="$global_bounds" | |
fi | |
# Add comma if not first source | |
if [[ $source_count -gt 0 ]]; then | |
sources_json="$sources_json," | |
source_folders_json="$source_folders_json," | |
fi | |
# Add source to JSON | |
sources_json="$sources_json | |
\"$source_name\": { | |
\"format\": \"jpg\", | |
\"name\": \"$name-$source_name\", | |
\"version\": \"2.0\", | |
\"type\": \"raster\", | |
\"minzoom\": $minzoom, | |
\"maxzoom\": $maxzoom, | |
\"scheme\": \"xyz\", | |
\"bounds\": $bounds, | |
\"center\": [0, 0, 6], | |
\"tiles\": [\"smp://maps.v1/s/0/{z}/{x}/{y}.jpg\"] | |
}" | |
# Add layer to JSON | |
layers_json="$layers_json, | |
{ | |
\"id\": \"raster-$source_name\", | |
\"type\": \"raster\", | |
\"source\": \"$source_name\", | |
\"paint\": { | |
\"raster-opacity\": 1 | |
} | |
}" | |
# Add to source folders metadata | |
source_folders_json="$source_folders_json | |
\"$source_name\": \"0\"" | |
source_count=$((source_count + 1)) | |
fi | |
done | |
fi | |
# Calculate center from highest zoom level extent | |
if [[ -n "$highest_zoom_extent" && "$highest_zoom_extent" != "null" ]]; then | |
center_coords=$(calculate_center "$highest_zoom_extent" 2>/dev/null || echo "$center_coords") | |
echo "Using center from highest zoom level ($highest_zoom): $center_coords" | |
fi | |
# Calculate overall bounds from highest zoom or use global bounds | |
if [[ -n "$highest_zoom_extent" && "$highest_zoom_extent" != "null" ]]; then | |
global_bounds=$(convert_bounds "$highest_zoom_extent" 2>/dev/null || echo "$global_bounds") | |
fi | |
# Fallback: if no JSON files found, create default source | |
if [[ $source_count -eq 0 ]]; then | |
echo "No JSON files found, creating default source..." | |
sources_json=" | |
\"mbtiles-source\": { | |
\"format\": \"jpg\", | |
\"name\": \"$name\", | |
\"version\": \"2.0\", | |
\"type\": \"raster\", | |
\"minzoom\": 0, | |
\"maxzoom\": $max_zoom, | |
\"scheme\": \"xyz\", | |
\"bounds\": $global_bounds, | |
\"center\": [0, 0, 6], | |
\"tiles\": [\"smp://maps.v1/s/0/{z}/{x}/{y}.jpg\"] | |
}" | |
layers_json=" | |
{ | |
\"id\": \"raster\", | |
\"type\": \"raster\", | |
\"source\": \"mbtiles-source\", | |
\"paint\": { | |
\"raster-opacity\": 1 | |
} | |
}" | |
source_folders_json=" | |
\"mbtiles-source\": \"0\"" | |
fi | |
# Generate style.json directly (avoiding template replacement complexity) | |
echo "Generating style.json..." | |
# Calculate default zoom | |
default_zoom=$(echo "scale=0; $highest_zoom - 3" | bc 2>/dev/null || echo "11") | |
if [[ $default_zoom -lt 1 ]]; then | |
default_zoom=11 | |
fi | |
# Create the complete style.json | |
cat > "$tmp_dir/style.json" << EOF | |
{ | |
"version": 8, | |
"name": "$name", | |
"sources": {$sources_json | |
}, | |
"layers": [ | |
{ | |
"id": "background", | |
"type": "background", | |
"paint": { | |
"background-color": "white" | |
} | |
},$layers_json | |
], | |
"metadata": { | |
"smp:bounds": $global_bounds, | |
"smp:maxzoom": $max_zoom, | |
"smp:sourceFolders": {$source_folders_json | |
} | |
}, | |
"center": $center_coords, | |
"zoom": $default_zoom | |
} | |
EOF | |
# Create output directory | |
mkdir -p "$output_dir" | |
# Create the SMP file (zip with .smp extension) | |
echo "Creating SMP file..." | |
# Show what we're about to zip | |
echo "Contents in temp directory:" | |
find "$tmp_dir" -type f | wc -l | |
echo "files total" | |
# Create zip from inside the temp directory to avoid path issues | |
cd "$tmp_dir" | |
if zip -r "../${output_dir}/${name}.smp" * > /dev/null 2>&1; then | |
echo "ZIP creation successful" | |
else | |
echo "Error: ZIP creation failed" | |
cd - > /dev/null | |
rm -rf "$tmp_dir" | |
exit 1 | |
fi | |
cd - > /dev/null | |
# Verify file was created before cleanup | |
if [[ -f "${output_dir}/${name}.smp" ]]; then | |
echo "Process completed successfully!" | |
echo "Output file: ${output_dir}/${name}.smp" | |
echo "File size: $(du -h "${output_dir}/${name}.smp" | cut -f1)" | |
else | |
echo "Error: Output file was not created" | |
rm -rf "$tmp_dir" | |
exit 1 | |
fi | |
# Clean up temporary directory | |
rm -rf "$tmp_dir" | |
break | |
else | |
echo "Invalid selection." | |
fi | |
done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": 8, | |
"name": "{{NAME}}", | |
"sources": {{SOURCES}}, | |
"layers": [ | |
{ | |
"id": "background", | |
"type": "background", | |
"paint": { | |
"background-color": "white" | |
} | |
}{{LAYERS}} | |
], | |
"metadata": { | |
"smp:bounds": {{BOUNDS}}, | |
"smp:maxzoom": {{MAX_ZOOM}}, | |
"smp:sourceFolders": {{SOURCE_FOLDERS}} | |
}, | |
"center": {{CENTER}}, | |
"zoom": {{DEFAULT_ZOOM}} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment