Last active
September 29, 2025 07:11
-
-
Save ashnair1/117bbc992793d3614d405b37ea9a6039 to your computer and use it in GitHub Desktop.
Convert XYZ to TMS
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
| 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