Skip to content

Instantly share code, notes, and snippets.

@ashnair1
Last active September 29, 2025 07:11
Show Gist options
  • Select an option

  • Save ashnair1/117bbc992793d3614d405b37ea9a6039 to your computer and use it in GitHub Desktop.

Select an option

Save ashnair1/117bbc992793d3614d405b37ea9a6039 to your computer and use it in GitHub Desktop.
Convert XYZ to TMS
import os
import shutil
from xml.etree.ElementTree import Element, SubElement, ElementTree
import argparse
# Mercator projection measures the Earth in meters, and the TMS format uses a specific tiling scheme.
EARTH_CIRCUMFERENCE = 40075016.68557849 # in meters
TILE_SIZE = 256 # in pixels
def convert_xyz_to_tms(xyz_dir, tms_dir, max_zoom, inplace=False):
for z in range(max_zoom + 1):
z_path = os.path.join(xyz_dir, str(z))
if not os.path.isdir(z_path):
continue
print(f"[Z={z}] Processing...")
for x in os.listdir(z_path):
x_path = os.path.join(z_path, x)
if not os.path.isdir(x_path):
continue
for y_file in os.listdir(x_path):
y_name, ext = os.path.splitext(y_file)
if not y_name.isdigit():
continue
xyz_y = int(y_name)
tms_y = ((1 << z) - 1) - xyz_y # 2^z - 1 is the maximum y value at zoom level z
new_file = f"{tms_y}{ext}"
src_path = os.path.join(x_path, y_file)
dst_x_path = os.path.join(tms_dir if not inplace else x_path, str(z), x) if not inplace else x_path
dst_path = os.path.join(dst_x_path, new_file)
os.makedirs(dst_x_path, exist_ok=True)
if inplace:
if src_path != dst_path:
os.rename(src_path, dst_path)
else:
shutil.copy2(src_path, dst_path)
def generate_tilemapresource_xml(output_path, tile_format, max_zoom):
root = Element("TileMap", version="1.0.0", tilemapservice="http://localhost/")
SubElement(root, "Title").text = "TMS Tiles"
SubElement(root, "Abstract")
SubElement(root, "SRS").text = "EPSG:3857"
SubElement(root, "BoundingBox", minx="-20037508.34", miny="-20037508.34",
maxx="20037508.34", maxy="20037508.34")
SubElement(root, "Origin", x="-20037508.34", y="-20037508.34")
SubElement(root, "TileFormat", attrib={"width": str(TILE_SIZE), "height": str(TILE_SIZE), "mime-type": f"image/{tile_format}", "extension": tile_format})
tilesets = SubElement(root, "TileSets", profile="global-mercator")
for z in range(1, max_zoom + 1):
#upp = 156543.03392804097 / (2 ** z)
upp = EARTH_CIRCUMFERENCE / (TILE_SIZE * (2 ** z)) # Units per pixel at zoom level z
SubElement(tilesets, "TileSet", attrib={"href": str(z), "units-per-pixel": str(upp), "order": str(z)})
tree = ElementTree(root)
tree.write(output_path, encoding="utf-8", xml_declaration=True)
print(f"Generated tilemapresource.xml at {output_path}")
def main():
parser = argparse.ArgumentParser(description="Convert XYZ tiles to TMS format.")
parser.add_argument("input_dir", help="Path to the XYZ directory")
parser.add_argument("max_zoom", type=int, help="Maximum zoom level")
parser.add_argument("--inplace", action="store_true", help="Convert in place")
parser.add_argument("--output-dir", help="Output directory (if not inplace)")
parser.add_argument("--tile-format", default="png", choices=["png", "jpeg", "jpg"], help="Tile image format")
args = parser.parse_args()
if args.inplace:
tms_dir = args.input_dir
else:
if not args.output_dir:
parser.error("Must specify --output-dir when not using --inplace")
tms_dir = args.output_dir
convert_xyz_to_tms(args.input_dir, tms_dir, args.max_zoom, inplace=args.inplace)
xml_path = os.path.join(tms_dir, "tilemapresource.xml")
generate_tilemapresource_xml(xml_path, tile_format=args.tile_format, max_zoom=args.max_zoom)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment