#!/usr/bin/env python3 """Local tile server for HK aerial map. Translates /tiles/{z}/{x}/{y}.jpg -> tiles/18_{x}_{y}.jpg""" import http.server import socketserver import os import re import webbrowser import threading from pathlib import Path PORT = 8080 SCRIPT_DIR = Path(__file__).parent TRANSPARENT_GIF = ( b'GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00' b'!\xf9\x04\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01' b'\x00\x00\x02\x02D\x01\x00;' ) TILE_PATTERN = re.compile( r'^/(tiles(?:_annotated)?)/(\d+)/(\d+)/(\d+)\.(jpg|png|gif)$' ) class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self): path = self.path.split('?')[0] m = TILE_PATTERN.match(path) if m: layer, z, x, y, ext = m.groups() # Tiles are stored as 18_x_y.jpg regardless of requested zoom flat = SCRIPT_DIR / layer / f'18_{x}_{y}.jpg' if flat.exists(): data = flat.read_bytes() self.send_response(200) self.send_header('Content-Type', 'image/jpeg') self.send_header('Content-Length', len(data)) self.send_header('Cache-Control', 'public, max-age=3600') self.end_headers() self.wfile.write(data) else: self.send_response(200) self.send_header('Content-Type', 'image/gif') self.send_header('Content-Length', len(TRANSPARENT_GIF)) self.end_headers() self.wfile.write(TRANSPARENT_GIF) return super().do_GET() def log_message(self, fmt, *args): # Only log non-tile requests so output stays readable if not TILE_PATTERN.match(self.path.split('?')[0]): print(f' {self.address_string()} {fmt % args}') def main(): os.chdir(SCRIPT_DIR) with socketserver.TCPServer(('', PORT), Handler) as httpd: httpd.allow_reuse_address = True url = f'http://localhost:{PORT}/map.html' print(f'HK Aerial Map → {url}') print('Press Ctrl+C to stop.\n') threading.Timer(0.4, webbrowser.open, args=(url,)).start() try: httpd.serve_forever() except KeyboardInterrupt: print('\nServer stopped.') if __name__ == '__main__': main()