├── .python-version ├── gcj_rectify_server ├── __init__.py ├── fetch.py ├── main.py ├── rectify.py ├── transform.py └── utils.py ├── gcj_rectify_plugin ├── images │ ├── icon.png │ ├── start.svg │ ├── stop.svg │ ├── amap.svg │ └── tile.svg ├── __init__.py ├── metadata.txt ├── qgis_utils.py ├── server.py └── plugin.py ├── README.md ├── pyproject.toml ├── LICENSE ├── .gitignore └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /gcj_rectify_server/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.4" 2 | 3 | from .main import app 4 | 5 | __all__ = ["app"] 6 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/gcj-rectify/main/gcj_rectify_plugin/images/icon.png -------------------------------------------------------------------------------- /gcj_rectify_plugin/images/start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/images/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import GCJRectifyPlugin 2 | 3 | 4 | def classFactory(iface): 5 | """QGIS Plugin""" 6 | return GCJRectifyPlugin(iface) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcj-rectify 2 | 3 | Rectify the map from GCJ-02 to WGS-84 coordinate system 4 | 5 | ## 运行服务 6 | 7 | 开发模式 8 | ```bash 9 | uv run uvicorn gcj_rectify_server:app --reload 10 | ``` 11 | 12 | 生产模式 13 | ```bash 14 | uv run uvicorn gcj_rectify_server:app --host 0.0.0.0 --port 8000 15 | ``` 16 | 17 | 直接使用 `uvx` 运行 18 | 19 | ```bash 20 | uvx gcj-rectify 21 | ``` 22 | 23 | ## 缓存位置设置 24 | 25 | 缓存目录默认为`/cache`, 通过环境变量 `GCJRE_CACHE` 来设置缓存目录 26 | 27 | 编辑缓存目录下的`maps.json`来增减地图 -------------------------------------------------------------------------------- /gcj_rectify_plugin/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=GCJRectify 3 | qgisMinimumVersion=3.14 4 | description=GCJ 坐标瓦片地图(无偏)加载 5 | about=GCJ 坐标高德瓦片地图(无偏)加载,启动本地服务后方可添加高德图层.仅供测试:) 6 | version=0.1.0-dev 7 | tags=basemap, xyz, 高德地图 8 | icon=images/icon.png 9 | experimental=False 10 | deprecated=False 11 | plugin_dependencies=gcj-rectify 12 | author=liuxspro 13 | email=liuxspro@gmail.com 14 | homepage=https://github.com/liuxspro/gcj-rectify 15 | tracker=https://github.com/liuxspro/gcj-rectify/issues 16 | repository=https://github.com/liuxspro/gcj-rectify 17 | changelog= -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [tool.hatch.build.targets.wheel] 6 | packages = ["gcj_rectify_server"] 7 | 8 | 9 | [project] 10 | name = "gcj-rectify" 11 | # version = "0.1.0" 12 | authors = [{ name = "liuxspro", email = "liuxspro@gmail.com" }] 13 | maintainers = [{ name = "liuxspro", email = "liuxspro@gmail.com" }] 14 | description = "GCJ Rectify Server" 15 | license = "MIT" 16 | readme = "README.md" 17 | requires-python = ">=3.12" 18 | dependencies = [ 19 | "fastapi>=0.116.1", 20 | "httpx>=0.28.1", 21 | "pillow>=11.3.0", 22 | "uvicorn>=0.35.0", 23 | ] 24 | dynamic = ["version"] 25 | 26 | 27 | [project.scripts] 28 | gcj-rectify = "gcj_rectify_server.main:run" 29 | 30 | [tool.hatch.version] 31 | path = "gcj_rectify_server/__init__.py" 32 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/qgis_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from qgis.core import QgsProject, QgsRasterLayer, QgsMessageLog, Qgis 4 | 5 | PluginDir = Path(__file__).parent 6 | CACHE_DIR = PluginDir.joinpath("./cache") 7 | 8 | 9 | def log_message(message): 10 | QgsMessageLog.logMessage(f"{message}", "GCJRectify", Qgis.Info) 11 | 12 | 13 | def add_raster_layer(uri: str, name: str, provider_type: str = "wms") -> None: 14 | """QGIS 添加栅格图层 15 | 16 | Args: 17 | uri (str): 栅格图层uri 18 | name (str): 栅格图层名称 19 | provider_type(str): 栅格图层类型(wms,arcgismapserver) 20 | Reference: https://qgis.org/pyqgis/3.32/core/QgsRasterLayer.html 21 | """ 22 | raster_layer = QgsRasterLayer(uri, name, provider_type) 23 | if raster_layer.isValid(): 24 | QgsProject.instance().addMapLayer(raster_layer) 25 | else: 26 | print(f"无效的图层 invalid Layer\n{uri}") 27 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/images/amap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/images/tile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 liuxspro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gcj_rectify_server/fetch.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from httpx import AsyncClient 4 | 5 | # 全局异步HTTP客户端 6 | _async_client: Optional[AsyncClient] = None 7 | 8 | 9 | def get_async_client() -> AsyncClient: 10 | """获取或创建异步HTTP客户端""" 11 | global _async_client 12 | if _async_client is None: 13 | _async_client = AsyncClient(timeout=30.0) 14 | return _async_client 15 | 16 | 17 | def close_async_client(): 18 | """关闭异步HTTP客户端(同步版本)""" 19 | global _async_client 20 | if _async_client is not None: 21 | # 强制设置为 None,让下次调用时重新创建 22 | _async_client = None 23 | 24 | 25 | async def close_async_client_async(): 26 | """关闭异步HTTP客户端(异步版本)""" 27 | global _async_client 28 | if _async_client is not None: 29 | await _async_client.aclose() 30 | _async_client = None 31 | 32 | 33 | def reset_async_client(): 34 | """重置异步HTTP客户端,强制重新创建""" 35 | global _async_client 36 | if _async_client is not None: 37 | _async_client = None 38 | 39 | 40 | async def fetch_tile(url: str) -> bytes: 41 | """ 42 | Fetch a tile image from the specified URL using an asynchronous HTTP client. 43 | 44 | Args: 45 | url (str): The URL to fetch the tile from. 46 | 47 | Returns: 48 | bytes: The content of the url and its content type. 49 | Raises: 50 | Exception: If the request fails or the response status is not 200. 51 | """ 52 | try: 53 | client = get_async_client() 54 | async with client.stream("GET", url) as response: 55 | if response.status_code != 200: 56 | raise Exception(f"Failed to fetch tile from {url}") 57 | content = await response.aread() 58 | return content 59 | except Exception as e: 60 | # 如果出现事件循环相关错误,重置客户端并重试一次 61 | if "Event loop is closed" in str(e) or "RuntimeError" in str(e): 62 | reset_async_client() 63 | client = get_async_client() 64 | async with client.stream("GET", url) as response: 65 | if response.status_code != 200: 66 | raise Exception(f"Failed to fetch tile from {url}") 67 | content = await response.aread() 68 | return content 69 | else: 70 | raise e 71 | -------------------------------------------------------------------------------- /gcj_rectify_server/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from contextlib import asynccontextmanager 3 | from pathlib import Path 4 | 5 | import uvicorn 6 | from fastapi import FastAPI, Response, Request 7 | 8 | from .fetch import reset_async_client 9 | from .rectify import get_tile_gcj_cached, get_tile_wgs_cached 10 | from .utils import get_cache_dir, get_maps 11 | 12 | 13 | @asynccontextmanager 14 | async def lifespan(app: FastAPI): 15 | """应用生命周期管理 16 | 17 | Args: 18 | app (FastAPI): 19 | """ 20 | # 启动时执行 21 | # 在启动服务器前重置异步客户端,确保使用新的事件循环 22 | reset_async_client() 23 | yield 24 | # 关闭时执行 25 | from .fetch import close_async_client_async 26 | 27 | await close_async_client_async() 28 | 29 | 30 | app = FastAPI(lifespan=lifespan) 31 | app.state.cache_dir = get_cache_dir() 32 | 33 | print(f"Cache Dir: {app.state.cache_dir}") 34 | print(f"Map Config: {app.state.cache_dir.joinpath("maps.json")}") 35 | 36 | 37 | @app.get("/") 38 | def index(): 39 | return {"message": "Server is Running"} 40 | 41 | 42 | @app.get("/config") 43 | def get_config(request: Request): 44 | return { 45 | "cache_dir": str(request.app.state.cache_dir), 46 | "maps": get_maps(request.app.state.cache_dir), 47 | } 48 | 49 | 50 | @app.get("/tiles/{map_id}/{z}/{x}/{y}") 51 | async def tile(map_id: str, z: int, x: int, y: int, request: Request): 52 | """ 53 | Get a tile image for the specified map ID, zoom level, and row/column numbers. 54 | 55 | Args: 56 | map_id (str): The ID of the map. 57 | z (int): Zoom level. 58 | x (int): Tile column number. 59 | y (int): Tile row number. 60 | request: Fastapi Request 61 | """ 62 | state_cache_dir = request.app.state.cache_dir 63 | 64 | if z <= 9: 65 | # For zoom levels 9 and below, use GCJ02 tiles directly 66 | img_bytes = await get_tile_gcj_cached(x, y, z, map_id, state_cache_dir) 67 | else: 68 | img_bytes = await get_tile_wgs_cached(x, y, z, map_id, state_cache_dir) 69 | if img_bytes is None: 70 | # 如果获取瓦片失败,返回空图片或错误响应 71 | return Response(status_code=500, content="Failed to fetch tile") 72 | return Response(content=img_bytes, media_type="image/png") 73 | 74 | 75 | def run(host: str = "0.0.0.0", port: int = 8000): 76 | """运行 GCJ Rectify 服务器 77 | 78 | Args: 79 | host: 服务器主机地址,默认为0.0.0.0 80 | port: 服务器端口,默认为8000 81 | """ 82 | # 解析命令行参数 83 | parser = argparse.ArgumentParser(description="GCJ Rectify 服务器") 84 | parser.add_argument("--host", default=host, help="服务器主机地址 (默认: 0.0.0.0)") 85 | parser.add_argument( 86 | "--port", type=int, default=port, help="服务器端口 (默认: 8000)" 87 | ) 88 | 89 | args = parser.parse_args() 90 | 91 | print(f"Server Runing At: http://{args.host}:{args.port}") 92 | uvicorn.run(app, host=args.host, port=args.port) 93 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/server.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from typing import Optional 4 | 5 | from fastapi import FastAPI 6 | from uvicorn import Server, Config 7 | 8 | from .qgis_utils import log_message 9 | 10 | 11 | class ServerManager: 12 | def __init__(self, _app: FastAPI, host: str = "0.0.0.0", port: int = 8080): 13 | self.app = _app 14 | self.host = host 15 | self.port = port 16 | self.server: Optional[Server] = None 17 | self.server_thread: Optional[threading.Thread] = None 18 | self._is_running = False 19 | 20 | def is_running(self) -> bool: 21 | """检查服务器是否正在运行""" 22 | return self._is_running and self.server_thread and self.server_thread.is_alive() 23 | 24 | def start(self) -> bool: 25 | # 如果服务器已在运行,先停止 26 | if self.is_running(): 27 | self.stop() 28 | 29 | try: 30 | # 创建服务器配置 31 | config = Config( 32 | app=self.app, 33 | host=self.host, 34 | port=self.port, 35 | lifespan="on", 36 | log_config=None, 37 | ) 38 | self.server = Server(config) 39 | 40 | # 创建并启动服务器线程 41 | self.server_thread = threading.Thread(target=self._run_server) 42 | self.server_thread.daemon = True 43 | self.server_thread.start() 44 | 45 | # 等待服务器启动(最多5秒) 46 | start_time = time.time() 47 | while not self._is_running and (time.time() - start_time) < 5: 48 | time.sleep(0.1) 49 | 50 | if self._is_running: 51 | return True 52 | else: 53 | return False 54 | except Exception as e: 55 | log_message(f"Error: {e}") 56 | return False 57 | 58 | def _run_server(self): 59 | """内部方法:运行服务器""" 60 | try: 61 | self._is_running = True 62 | # 确保在新线程中有新的事件循环 63 | self.server.run() 64 | except Exception as e: 65 | print(f"🚨 Server crashed: {str(e)}") 66 | finally: 67 | self._is_running = False 68 | 69 | def stop(self, timeout: float = 5.0) -> bool | None: 70 | if not self.is_running(): 71 | return False 72 | 73 | try: 74 | # 通知服务器退出 75 | if self.server: 76 | self.server.should_exit = True 77 | 78 | # 等待线程结束 79 | if self.server_thread: 80 | self.server_thread.join(timeout=timeout) 81 | 82 | # 如果线程仍然存活,强制终止 83 | if self.server_thread and self.server_thread.is_alive(): 84 | if self.server: 85 | self.server.force_exit = True 86 | self.server_thread.join(timeout=1.0) 87 | # 最后尝试强制终止 88 | if self.server_thread.is_alive(): 89 | return False 90 | return True 91 | except Exception as e: 92 | print(f"Error: {e}") 93 | return False 94 | finally: 95 | # 清理资源 96 | self.server = None 97 | self.server_thread = None 98 | self._is_running = False 99 | -------------------------------------------------------------------------------- /gcj_rectify_server/rectify.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from io import BytesIO 3 | from pathlib import Path 4 | 5 | from PIL import Image 6 | 7 | from .fetch import fetch_tile 8 | from .utils import ( 9 | xyz_to_bbox, 10 | wgsbbox_to_gcjbbox, 11 | lonlat_to_xyz, 12 | image_to_bytes, 13 | bytes_to_image, 14 | get_maps, 15 | ) 16 | 17 | 18 | async def get_tile_gcj(x: int, y: int, z: int, mapid: str, map_data) -> bytes: 19 | """ 20 | 获取指定行列号的瓦片,这里下载的是原始瓦片(GCJ02 坐标系)。 21 | Args: 22 | x (int): Tile X coordinate. 23 | y (int): Tile Y coordinate. 24 | z (int): Zoom level. 25 | mapid (str): Map Id 26 | map_data: GCJ Maps 27 | Returns: 28 | bytes: Tile image bytes. 29 | """ 30 | url = map_data[mapid]["url"] 31 | if "-y" in url: 32 | # 如果 URL 中包含 -y,认为是TMS格式,需要调整 Y 值 33 | url = url.replace("-y", "y") 34 | url = url.format(x=x, y=(2**z - 1 - y), z=z) 35 | else: 36 | url = url.format(x=x, y=y, z=z) 37 | 38 | # 使用异步HTTP客户端获取瓦片 39 | content = await fetch_tile(url) 40 | 41 | return content 42 | 43 | 44 | async def get_tile_gcj_cached( 45 | x: int, y: int, z: int, mapid: str, cache_dir: Path 46 | ) -> bytes: 47 | """ 48 | 获取指定瓦片的图像,使用缓存。 49 | Args: 50 | x (int): Tile X coordinate. 51 | y (int): Tile Y coordinate. 52 | z (int): Zoom level. 53 | mapid (str): Map Id 54 | cache_dir (str): 缓存目录 55 | Returns: 56 | Image: Tile image. 57 | """ 58 | tile_file_path = cache_dir.joinpath(f"{mapid}/GCJ/{z}/{x}/{y}.png") 59 | if tile_file_path.exists(): 60 | # print(f"从缓存中获取了瓦片: {tile_file_path}") 61 | image = Image.open(tile_file_path) 62 | return image_to_bytes(image) 63 | # 如果缓存不存在,则下载瓦片并保存到缓存 64 | tile_file_path.parent.mkdir(parents=True, exist_ok=True) 65 | 66 | map_data = get_maps(cache_dir) 67 | image_bytes = await get_tile_gcj(x, y, z, mapid, map_data) 68 | image = bytes_to_image(image_bytes) 69 | image.save(tile_file_path, "PNG") 70 | 71 | return image_bytes 72 | 73 | 74 | async def get_tile_wgs( 75 | x: int, y: int, z: int, mapid: str, cache_dir: Path 76 | ) -> bytes | None: 77 | """ 78 | 获取瓦片(调整为 WGS84 坐标系) 79 | """ 80 | if z <= 9: 81 | return None 82 | 83 | wgs_bbox = xyz_to_bbox(x, y, z) 84 | gcj_bbox = wgsbbox_to_gcjbbox(wgs_bbox) 85 | left_upper, right_lower = gcj_bbox 86 | 87 | # 计算左上角和右下角的瓦片行列号 88 | x_min, y_min = lonlat_to_xyz(left_upper[0], left_upper[1], z) # 左上角 89 | x_max, y_max = lonlat_to_xyz(right_lower[0], right_lower[1], z) # 右下角 90 | 91 | # 创建任务列表,异步获取所有需要的瓦片 92 | tasks = [] 93 | for ax in range(x_min, x_max + 1): 94 | for ay in range(y_min, y_max + 1): 95 | tasks.append(get_tile_gcj_cached(ax, ay, z, mapid, cache_dir)) 96 | 97 | # 并发执行所有瓦片下载任务 98 | tiles = await asyncio.gather(*tasks) 99 | tile_images = [Image.open(BytesIO(content)) for content in tiles] 100 | 101 | # 拼合瓦片 102 | composite = Image.new( 103 | "RGBA", ((x_max - x_min + 1) * 256, (y_max - y_min + 1) * 256) 104 | ) 105 | 106 | tile_index = 0 107 | for i, ax in enumerate(range(x_min, x_max + 1)): 108 | for j, ay in enumerate(range(y_min, y_max + 1)): 109 | tile = tile_images[tile_index] 110 | if tile: 111 | composite.paste(tile, (i * 256, j * 256)) 112 | tile_index += 1 113 | 114 | # 计算拼合后的瓦片范围 115 | megred_bbox = xyz_to_bbox(x_min, y_min, z)[0], xyz_to_bbox(x_max, y_max, z)[1] 116 | 117 | x_range = megred_bbox[1][0] - megred_bbox[0][0] 118 | y_range = megred_bbox[0][1] - megred_bbox[1][1] 119 | 120 | left_percent = (gcj_bbox[0][0] - megred_bbox[0][0]) / x_range 121 | top_percent = (megred_bbox[0][1] - gcj_bbox[0][1]) / y_range 122 | img_width, img_height = composite.size 123 | # 裁剪选区(left, top, right, bottom) 124 | crop_bbox = ( 125 | int(left_percent * img_width), 126 | int(top_percent * img_height), 127 | int(left_percent * img_width) + 256, 128 | int(top_percent * img_height) + 256, 129 | ) 130 | 131 | # 从拼合的瓦片中裁剪出对应的区域 132 | croped_image = composite.crop(crop_bbox) 133 | return image_to_bytes(croped_image) 134 | 135 | 136 | async def get_tile_wgs_cached( 137 | x: int, y: int, z: int, mapid: str, cache_dir: Path 138 | ) -> bytes: 139 | """ 140 | 获取指定瓦片的图像,使用缓存。 141 | Args: 142 | x (int): Tile X coordinate. 143 | y (int): Tile Y coordinate. 144 | z (int): Zoom level. 145 | mapid (str): Map Id 146 | cache_dir (str): 缓存目录 147 | Returns: 148 | Image: Tile image. 149 | """ 150 | tile_file_path = cache_dir.joinpath(f"{mapid}/WGS/{z}/{x}/{y}.png") 151 | if tile_file_path.exists(): 152 | image = Image.open(tile_file_path) 153 | return image_to_bytes(image) 154 | 155 | tile_file_path.parent.mkdir(parents=True, exist_ok=True) 156 | image_bytes = await get_tile_wgs(x, y, z, mapid, cache_dir) 157 | image = bytes_to_image(image_bytes) 158 | image.save(tile_file_path, "PNG") 159 | 160 | return image_bytes 161 | -------------------------------------------------------------------------------- /gcj_rectify_server/transform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ########################################################################################## 3 | """ 4 | /*************************************************************************** 5 | OffsetWGS84Core 6 | A QGIS plugin 7 | Class with methods for geometry and attributes processing 8 | ------------------- 9 | begin : 2016-10-11 10 | git sha : $Format:%H$ 11 | copyright : (C) 2017 by sshuair 12 | email : sshuair@gmail.com 13 | ***************************************************************************/ 14 | 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | """ 24 | from __future__ import print_function 25 | 26 | import math 27 | 28 | ########################################################################################## 29 | from builtins import zip 30 | from math import atan2, cos, fabs 31 | from math import pi as PI 32 | from math import sin, sqrt 33 | 34 | # from numba import jit 35 | 36 | 37 | # =================================================sshuair============================================================= 38 | # define ellipsoid 39 | a = 6378245.0 40 | f = 1 / 298.3 41 | b = a * (1 - f) 42 | ee = 1 - (b * b) / (a * a) 43 | 44 | 45 | # check if the point in china 46 | def outOfChina(lng, lat): 47 | return not (72.004 <= lng <= 137.8347 and 0.8293 <= lat <= 55.8271) 48 | 49 | 50 | # @jit 51 | def geohey_transformLat(x, y): 52 | ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x)) 53 | ret = ret + (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 54 | ret = ret + (20.0 * sin(y * PI) + 40.0 * sin(y / 3.0 * PI)) * 2.0 / 3.0 55 | ret = ret + (160.0 * sin(y / 12.0 * PI) + 320.0 * sin(y * PI / 30.0)) * 2.0 / 3.0 56 | return ret 57 | 58 | 59 | # @jit 60 | def geohey_transformLon(x, y): 61 | ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x)) 62 | ret = ret + (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 63 | ret = ret + (20.0 * sin(x * PI) + 40.0 * sin(x / 3.0 * PI)) * 2.0 / 3.0 64 | ret = ret + (150.0 * sin(x / 12.0 * PI) + 300.0 * sin(x * PI / 30.0)) * 2.0 / 3.0 65 | return ret 66 | 67 | 68 | # @jit 69 | def wgs2gcj(wgsLon, wgsLat): 70 | if outOfChina(wgsLon, wgsLat): 71 | return wgsLon, wgsLat 72 | dLat = geohey_transformLat(wgsLon - 105.0, wgsLat - 35.0) 73 | dLon = geohey_transformLon(wgsLon - 105.0, wgsLat - 35.0) 74 | radLat = wgsLat / 180.0 * PI 75 | magic = math.sin(radLat) 76 | magic = 1 - ee * magic * magic 77 | sqrtMagic = sqrt(magic) 78 | dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI) 79 | dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * PI) 80 | gcjLat = wgsLat + dLat 81 | gcjLon = wgsLon + dLon 82 | return (gcjLon, gcjLat) 83 | 84 | 85 | def gcj2wgs(gcjLon, gcjLat): 86 | g0 = (gcjLon, gcjLat) 87 | w0 = g0 88 | g1 = wgs2gcj(w0[0], w0[1]) 89 | # w1 = w0 - (g1 - g0) 90 | w1 = tuple([x[0] - (x[1] - x[2]) for x in zip(w0, g1, g0)]) 91 | # delta = w1 - w0 92 | delta = tuple([x[0] - x[1] for x in zip(w1, w0)]) 93 | while abs(delta[0]) >= 1e-6 or abs(delta[1]) >= 1e-6: 94 | w0 = w1 95 | g1 = wgs2gcj(w0[0], w0[1]) 96 | # w1 = w0 - (g1 - g0) 97 | w1 = tuple([x[0] - (x[1] - x[2]) for x in zip(w0, g1, g0)]) 98 | # delta = w1 - w0 99 | delta = tuple([x[0] - x[1] for x in zip(w1, w0)]) 100 | return w1 101 | 102 | 103 | def gcj2bd(gcjLon, gcjLat): 104 | z = sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * sin( 105 | gcjLat * PI * 3000.0 / 180.0 106 | ) 107 | theta = atan2(gcjLat, gcjLon) + 0.000003 * cos(gcjLon * PI * 3000.0 / 180.0) 108 | bdLon = z * cos(theta) + 0.0065 109 | bdLat = z * sin(theta) + 0.006 110 | return (bdLon, bdLat) 111 | 112 | 113 | def bd2gcj(bdLon, bdLat): 114 | x = bdLon - 0.0065 115 | y = bdLat - 0.006 116 | z = sqrt(x * x + y * y) - 0.00002 * sin(y * PI * 3000.0 / 180.0) 117 | theta = atan2(y, x) - 0.000003 * cos(x * PI * 3000.0 / 180.0) 118 | gcjLon = z * cos(theta) 119 | gcjLat = z * sin(theta) 120 | return (gcjLon, gcjLat) 121 | 122 | 123 | def wgs2bd(wgsLon, wgsLat): 124 | gcj = wgs2gcj(wgsLon, wgsLat) 125 | return gcj2bd(gcj[0], gcj[1]) 126 | 127 | 128 | def bd2wgs(bdLon, bdLat): 129 | gcj = bd2gcj(bdLon, bdLat) 130 | return gcj2wgs(gcj[0], gcj[1]) 131 | 132 | 133 | if __name__ == "__main__": 134 | # wgs2gcj 135 | # coord = (112, 40) 136 | # trans = WGS2GCJ() 137 | print(wgs2gcj(117.136230, 34.252676)) 138 | print(gcj2wgs(112.00678230985764, 40.00112245823686)) 139 | 140 | # gcj2wgs 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | #poetry.toml 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 115 | #pdm.lock 116 | #pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # pixi 121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 122 | #pixi.lock 123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 124 | # in the .venv directory. It is recommended not to include this directory in version control. 125 | .pixi 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .envrc 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Abstra 179 | # Abstra is an AI-powered process automation framework. 180 | # Ignore directories containing user credentials, local state, and settings. 181 | # Learn more at https://abstra.io/docs 182 | .abstra/ 183 | 184 | # Visual Studio Code 185 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 186 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 187 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 188 | # you could uncomment the following to ignore the entire vscode folder 189 | # .vscode/ 190 | 191 | # Ruff stuff: 192 | .ruff_cache/ 193 | 194 | # PyPI configuration file 195 | .pypirc 196 | 197 | # Cursor 198 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 199 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 200 | # refer to https://docs.cursor.com/context/ignore-files 201 | .cursorignore 202 | .cursorindexingignore 203 | 204 | # Marimo 205 | marimo/_static/ 206 | marimo/_lsp/ 207 | __marimo__/ 208 | 209 | cache/ 210 | .idea/ 211 | -------------------------------------------------------------------------------- /gcj_rectify_server/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from io import BytesIO 4 | from math import atan, cos, log, pi, sinh, tan 5 | from pathlib import Path 6 | 7 | from PIL import Image 8 | 9 | # 使用了来自 Geohey-Team 的 qgis-geohey-toolbox 插件中的转换算法 10 | # https://github.com/GeoHey-Team/qgis-geohey-toolbox 11 | from .transform import wgs2gcj 12 | 13 | gcj_maps = { 14 | "amap-vec": { 15 | "name": "高德地图 - 矢量地图", 16 | "url": "https://wprd02.is.autonavi.com//appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}", 17 | "min_zoom": 3, 18 | "max_zoom": 18, 19 | }, 20 | "amap-labels": { 21 | "name": "高德地图 - 矢量注记", 22 | "url": "http://wprd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}", 23 | "min_zoom": 3, 24 | "max_zoom": 18, 25 | }, 26 | "amap-sat": { 27 | "name": "高德地图 - 卫星影像", 28 | "url": "https://wprd02.is.autonavi.com//appmaptile?lang=zh_cn&size=1&scale=1&style=6&x={x}&y={y}&z={z}", 29 | "min_zoom": 3, 30 | "max_zoom": 18, 31 | }, 32 | "tencent-vec": { 33 | "name": "腾讯地图 - 矢量地图", 34 | "url": "http://rt0.map.gtimg.com/realtimerender?z={z}&x={x}&y={-y}&type=vector&style=0", 35 | "min_zoom": 3, 36 | "max_zoom": 18, 37 | }, 38 | "google-street": { 39 | "name": "Google - Road", 40 | "url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&hl=zh-CN", 41 | "min_zoom": 0, 42 | "max_zoom": 20, 43 | }, 44 | "google-lables": { 45 | "name": "Google - Labels", 46 | "url": "https://mt1.google.com/vt/lyrs=h&x={x}&y={y}&z={z}&hl=zh-CN", 47 | "min_zoom": 2, 48 | "max_zoom": 20, 49 | }, 50 | } 51 | 52 | 53 | def get_cache_dir() -> Path: 54 | """ 55 | Get the cache directory from the environment variable or default to the app directory. 56 | 57 | Returns: 58 | str: The path to the cache directory. 59 | """ 60 | env_cache = os.getenv("GCJRE_CACHE") 61 | if env_cache: 62 | # print(f"Using cache directory from environment: {env_cache}") 63 | env_cache_dir = Path(env_cache) 64 | env_cache_dir.mkdir(exist_ok=True) 65 | return env_cache_dir 66 | current_cache_dir = Path.cwd().joinpath("cache") 67 | current_cache_dir.mkdir(exist_ok=True) 68 | # print(f"Using current directory for cache: {current_cache_dir}") 69 | return current_cache_dir 70 | 71 | 72 | def init_map_config(config_path: Path): 73 | map_file_path = config_path.joinpath("maps.json") 74 | if not map_file_path.exists(): 75 | with open(str(map_file_path), "w", encoding="utf-8") as f: 76 | json.dump(gcj_maps, f, indent=2, ensure_ascii=False) 77 | 78 | 79 | def get_maps(config_path: Path): 80 | map_file_path = config_path.joinpath("maps.json") 81 | if not map_file_path.exists(): 82 | init_map_config(config_path) 83 | with open(map_file_path, "r", encoding="utf-8") as f: 84 | data = json.load(f) 85 | return data 86 | 87 | 88 | def bytes_to_image(content: bytes) -> Image: 89 | """ 90 | Convert bytes to a PIL Image. 91 | 92 | Args: 93 | content (bytes): Image data in bytes. 94 | 95 | Returns: 96 | Image: PIL Image object. 97 | """ 98 | return Image.open(BytesIO(content)) 99 | 100 | 101 | def image_to_bytes(image: Image, img_format: str = "PNG") -> bytes: 102 | """ 103 | Convert a PIL Image to bytes. 104 | 105 | Args: 106 | image (Image): PIL Image object. 107 | img_format (str): Format to save the image, default is "PNG". 108 | 109 | Returns: 110 | bytes: Image data in bytes. 111 | """ 112 | img_buffer = BytesIO() 113 | image.save(img_buffer, format=img_format) 114 | img_bytes = img_buffer.getvalue() 115 | img_buffer.close() 116 | return img_bytes 117 | 118 | 119 | def xyz_to_lonlat(x: int, y: int, z: int) -> tuple: 120 | """ 121 | 将XYZ瓦片坐标转换为经纬度(左上角点)。 122 | 123 | Args: 124 | x (int): Tile X coordinate. 125 | y (int): Tile Y coordinate. 126 | z (int): Zoom level. 127 | 128 | Returns: 129 | tuple: Longitude and latitude in degrees. 130 | """ 131 | n = 2.0**z 132 | lon_deg = x / n * 360.0 - 180.0 133 | lat_rad = atan(sinh(pi * (1 - 2 * y / n))) 134 | lat_deg = lat_rad * 180.0 / pi 135 | return lon_deg, lat_deg 136 | 137 | 138 | def lonlat_to_xyz(lon: float, lat: float, z: int) -> tuple: 139 | """ 140 | Convert longitude and latitude to XYZ tile coordinates. 141 | 142 | Args: 143 | lon (float): Longitude in degrees. 144 | lat (float): Latitude in degrees. 145 | z (int): Zoom level. 146 | 147 | Returns: 148 | tuple: Tile X and Y coordinates. 149 | """ 150 | n = 2.0**z 151 | x = (lon + 180.0) / 360.0 * n 152 | lat_rad = lat * pi / 180.0 153 | t = log(tan(lat_rad) + 1 / cos(lat_rad)) 154 | y = (1 - t / pi) * n / 2 155 | return int(x), int(y) 156 | 157 | 158 | def xyz_to_bbox(x, y, z): 159 | """ 160 | Convert XYZ tile coordinates to bounding box coordinates. 161 | 162 | Args: 163 | x (int): Tile X coordinate. 164 | y (int): Tile Y coordinate. 165 | z (int): Zoom level. 166 | 167 | Returns: 168 | tuple: Bounding box in the format (min_lon, min_lat, max_lon, max_lat). 169 | """ 170 | left_upper_lon, left_upper_lat = xyz_to_lonlat(x, y, z) 171 | right_lower_lon, right_lower_lat = xyz_to_lonlat(x + 1, y + 1, z) 172 | 173 | return (left_upper_lon, left_upper_lat), (right_lower_lon, right_lower_lat) 174 | 175 | 176 | def wgsbbox_to_gcjbbox(wgs_bbox): 177 | """ 178 | Convert WGS84 bounding box to GCJ02 bounding box. 179 | 180 | Args: 181 | wgs_bbox (tuple): Bounding box in the format (min_lon, min_lat, max_lon, max_lat). 182 | 183 | Returns: 184 | tuple: GCJ02 bounding box in the same format. 185 | """ 186 | left_upper, right_lower = wgs_bbox 187 | gcj_left_upper = wgs2gcj(left_upper[0], left_upper[1]) 188 | gcj_right_lower = wgs2gcj(right_lower[0], right_lower[1]) 189 | return gcj_left_upper, gcj_right_lower 190 | -------------------------------------------------------------------------------- /gcj_rectify_plugin/plugin.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from urllib.parse import quote 3 | 4 | from gcj_rectify_server import app 5 | from gcj_rectify_server.utils import get_maps 6 | from qgis.PyQt.QtGui import QIcon 7 | from qgis.PyQt.QtWidgets import ( 8 | QAction, 9 | QDialog, 10 | QVBoxLayout, 11 | QLabel, 12 | QPushButton, 13 | QLineEdit, 14 | QHBoxLayout, 15 | QFileDialog, 16 | QSpinBox, 17 | ) 18 | from qgis.core import QgsSettings 19 | 20 | from .qgis_utils import PluginDir, add_raster_layer, log_message, CACHE_DIR 21 | from .server import ServerManager 22 | 23 | tile_icon = QIcon(str(PluginDir / "images" / "tile.svg")) 24 | 25 | 26 | class SettingsDialog(QDialog): 27 | def __init__(self, parent=None): 28 | super().__init__(parent) 29 | self.conf = QgsSettings() 30 | self.port = self.conf.value("gcj-rectify/port", 8080, type=int) 31 | self.cache_dir = self.conf.value("gcj-rectify/cache_dir", str(CACHE_DIR)) 32 | self.setWindowTitle("设置") 33 | self.setMinimumWidth(600) 34 | layout = QVBoxLayout() 35 | 36 | # 端口设置 37 | port_layout = QHBoxLayout() 38 | port_label = QLabel("端口:") 39 | self.port_spin = QSpinBox() 40 | self.port_spin.setRange(1, 65535) 41 | self.port_spin.setValue(self.get_port()) 42 | port_layout.addWidget(port_label) 43 | port_layout.addWidget(self.port_spin) 44 | layout.addLayout(port_layout) 45 | 46 | # 缓存目录设置 47 | cache_layout = QHBoxLayout() 48 | cache_label = QLabel("缓存目录:") 49 | self.cache_edit = QLineEdit(self.get_cache_dir()) 50 | self.cache_edit.setPlaceholderText("请选择缓存目录") 51 | cache_btn = QPushButton("选择...") 52 | cache_btn.clicked.connect(self.choose_cache_dir) 53 | cache_layout.addWidget(cache_label) 54 | cache_layout.addWidget(self.cache_edit) 55 | cache_layout.addWidget(cache_btn) 56 | layout.addLayout(cache_layout) 57 | 58 | # 确认和关闭按钮 59 | btn_layout = QHBoxLayout() 60 | ok_btn = QPushButton("确定") 61 | ok_btn.clicked.connect(self.accept) 62 | cancel_btn = QPushButton("取消") 63 | cancel_btn.clicked.connect(self.reject) 64 | btn_layout.addWidget(ok_btn) 65 | btn_layout.addWidget(cancel_btn) 66 | layout.addLayout(btn_layout) 67 | 68 | self.setLayout(layout) 69 | 70 | def choose_cache_dir(self): 71 | dir_path = QFileDialog.getExistingDirectory(self, "选择缓存目录", "") 72 | if dir_path: 73 | self.cache_edit.setText(dir_path) 74 | 75 | def accept(self): 76 | super().accept() 77 | self.conf.setValue("gcj-rectify/port", self.port_spin.value()) 78 | self.conf.setValue("gcj-rectify/cache_dir", self.cache_edit.text()) 79 | log_message( 80 | f"设置已保存: 端口={self.port_spin.value()}, 缓存目录={self.cache_edit.text()}" 81 | ) 82 | 83 | def get_port(self): 84 | return self.conf.value("gcj-rectify/port", 8080, type=int) 85 | 86 | def get_cache_dir(self): 87 | return self.conf.value("gcj-rectify/cache_dir", str(CACHE_DIR), type=str) 88 | 89 | def get_settings(self): 90 | return { 91 | "port": self.port_spin.value(), 92 | "cache_dir": self.cache_edit.text(), 93 | } 94 | 95 | 96 | class GCJRectifyPlugin: 97 | def __init__(self, iface): 98 | self.settings_action = None 99 | self.conf = QgsSettings() 100 | self.app = app 101 | self.port = self.get_port() 102 | self.iface = iface 103 | self.server = ServerManager(self.app, port=self.port) 104 | # 添加动作列表 105 | self.actions = [] 106 | self.start_action = None 107 | self.add_map_cations = [] 108 | self.stop_action = None 109 | 110 | # 添加菜单 111 | self.menu = None 112 | 113 | def initGui(self): 114 | self.init_config() 115 | # 创建菜单 116 | self.menu = self.iface.mainWindow().menuBar().addMenu("&GCJ-Rectify") 117 | 118 | # 启动服务 Action 119 | icon = QIcon(str(PluginDir / "images" / "start.svg")) 120 | self.start_action = QAction(icon, "启动服务器", self.iface.mainWindow()) 121 | self.start_action.triggered.connect(self.start_server) 122 | 123 | self.actions.append(self.start_action) 124 | 125 | # 停止服务 Action 126 | self.stop_action = QAction( 127 | QIcon(str(PluginDir / "images" / "stop.svg")), 128 | "停止服务器", 129 | self.iface.mainWindow(), 130 | ) 131 | self.stop_action.triggered.connect(self.stop_server) 132 | self.actions.append(self.stop_action) 133 | 134 | # 添加 Action 到菜单 135 | self.menu.addAction(self.start_action) 136 | self.menu.addAction(self.stop_action) 137 | self.menu.addSeparator() 138 | 139 | # 设置 Action 140 | self.settings_action = QAction(QIcon(), "设置", self.iface.mainWindow()) 141 | self.settings_action.triggered.connect(self.show_settings_dialog) 142 | self.actions.append(self.settings_action) 143 | self.menu.addAction(self.settings_action) 144 | self.menu.addSeparator() 145 | 146 | # 添加地图 Action 147 | map_data = get_maps(Path(self.get_cache_dir())) 148 | for mapid in map_data.keys(): 149 | action = QAction( 150 | tile_icon, map_data[mapid]["name"], self.iface.mainWindow() 151 | ) 152 | action.triggered.connect( 153 | lambda _checked, mid=mapid: self.add_map(self.get_port(), mid) 154 | ) 155 | self.add_map_cations.append(action) 156 | 157 | for action in self.add_map_cations: 158 | self.actions.append(action) 159 | self.menu.addAction(action) 160 | 161 | # 初始状态:启动按钮可用,停止按钮不可用 162 | self.stop_action.setEnabled(False) 163 | for action in self.add_map_cations: 164 | action.setEnabled(False) 165 | 166 | log_message(f"GCJ-Rectify 插件初始化完成") 167 | log_message(f"端口: {self.port} 缓存目录: {self.get_cache_dir()}") 168 | # self.start_server() 169 | 170 | def init_config(self): 171 | log_message("初始化 GCJRectifyPlugin 插件...") 172 | if not self.conf.contains("gcj-rectify/port"): 173 | log_message("初始化配置文件") 174 | self.conf.setValue("gcj-rectify/port", 8080) 175 | self.conf.setValue("gcj-rectify/cache_dir", str(CACHE_DIR)) 176 | Path(self.get_cache_dir()).mkdir(exist_ok=True) 177 | self.app.state.cache_dir = Path(self.get_cache_dir()) 178 | 179 | def get_port(self): 180 | """获取当前端口""" 181 | return self.conf.value("gcj-rectify/port", 8080, type=int) 182 | 183 | def get_cache_dir(self): 184 | """获取当前缓存目录""" 185 | return self.conf.value("gcj-rectify/cache_dir", str(CACHE_DIR), type=str) 186 | 187 | def add_map(self, port, mapid): 188 | map_url = f"http://localhost:{port}/tiles/{mapid}/{{z}}/{{x}}/{{y}}" 189 | map_data = get_maps(Path(self.get_cache_dir()))[mapid] 190 | # URL编码处理 191 | encoded_url = quote(map_url, safe=":/?=") 192 | uri = f"type=xyz&url={encoded_url}&zmin={map_data['min_zoom']}&zmax={map_data['max_zoom']}" 193 | add_raster_layer(uri, map_data["name"]) 194 | 195 | def start_server(self): 196 | self.app.state.cache_dir = Path(self.get_cache_dir()) 197 | self.server.port = self.get_port() 198 | self.server.cache_dir = self.conf.value("gcj-rectify/cache_dir", str(CACHE_DIR)) 199 | if self.server.start(): 200 | log_message("✅ 服务器启动成功!") 201 | log_message(f"📦 缓存目录 {self.server.app.state.cache_dir}") 202 | log_message( 203 | f"🌐 API 文档地址:http://localhost:{self.server.port}/docs", 204 | ) 205 | # 更新UI状态 206 | self.start_action.setEnabled(False) 207 | self.settings_action.setEnabled(False) 208 | self.stop_action.setEnabled(True) 209 | for action in self.add_map_cations: 210 | action.setEnabled(True) 211 | else: 212 | log_message("❌ 服务器启动失败") 213 | 214 | def stop_server(self): 215 | if self.server.stop(): 216 | # 更新UI状态 217 | self.start_action.setEnabled(True) 218 | self.stop_action.setEnabled(False) 219 | self.settings_action.setEnabled(True) 220 | for action in self.add_map_cations: 221 | action.setEnabled(False) 222 | log_message("✅ 服务器已停止") 223 | 224 | def show_settings_dialog(self): 225 | # 默认值可根据实际情况传递 226 | dlg = SettingsDialog(self.iface.mainWindow()) 227 | dlg.exec_() 228 | 229 | def unload(self): 230 | """从QGIS界面卸载插件""" 231 | log_message("卸载 GCJ-Rectify 插件...") 232 | 233 | # 停止服务器 234 | self.stop_server() 235 | 236 | # 移除菜单 237 | if self.menu: 238 | self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction()) 239 | 240 | # 移除工具栏图标 241 | for action in self.actions: 242 | self.iface.removeToolBarIcon(action) 243 | 244 | log_message("GCJ-Rectify 插件已卸载") 245 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 9 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } 10 | wheels = [ 11 | { url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.10.0" 17 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 22 | ] 23 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6" } 24 | wheels = [ 25 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1" }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2025.8.3" 31 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 32 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407" } 33 | wheels = [ 34 | { url = "https://mirrors.aliyun.com/pypi/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5" }, 35 | ] 36 | 37 | [[package]] 38 | name = "click" 39 | version = "8.2.1" 40 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 41 | dependencies = [ 42 | { name = "colorama", marker = "sys_platform == 'win32'" }, 43 | ] 44 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202" } 45 | wheels = [ 46 | { url = "https://mirrors.aliyun.com/pypi/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" }, 47 | ] 48 | 49 | [[package]] 50 | name = "colorama" 51 | version = "0.4.6" 52 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 53 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } 54 | wheels = [ 55 | { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, 56 | ] 57 | 58 | [[package]] 59 | name = "fastapi" 60 | version = "0.116.1" 61 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 62 | dependencies = [ 63 | { name = "pydantic" }, 64 | { name = "starlette" }, 65 | { name = "typing-extensions" }, 66 | ] 67 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143" } 68 | wheels = [ 69 | { url = "https://mirrors.aliyun.com/pypi/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565" }, 70 | ] 71 | 72 | [[package]] 73 | name = "gcj-rectify" 74 | source = { editable = "." } 75 | dependencies = [ 76 | { name = "fastapi" }, 77 | { name = "httpx" }, 78 | { name = "pillow" }, 79 | { name = "uvicorn" }, 80 | ] 81 | 82 | [package.metadata] 83 | requires-dist = [ 84 | { name = "fastapi", specifier = ">=0.116.1" }, 85 | { name = "httpx", specifier = ">=0.28.1" }, 86 | { name = "pillow", specifier = ">=11.3.0" }, 87 | { name = "uvicorn", specifier = ">=0.35.0" }, 88 | ] 89 | 90 | [[package]] 91 | name = "h11" 92 | version = "0.16.0" 93 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 94 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } 95 | wheels = [ 96 | { url = "https://mirrors.aliyun.com/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, 97 | ] 98 | 99 | [[package]] 100 | name = "httpcore" 101 | version = "1.0.9" 102 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 103 | dependencies = [ 104 | { name = "certifi" }, 105 | { name = "h11" }, 106 | ] 107 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } 108 | wheels = [ 109 | { url = "https://mirrors.aliyun.com/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, 110 | ] 111 | 112 | [[package]] 113 | name = "httpx" 114 | version = "0.28.1" 115 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 116 | dependencies = [ 117 | { name = "anyio" }, 118 | { name = "certifi" }, 119 | { name = "httpcore" }, 120 | { name = "idna" }, 121 | ] 122 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } 123 | wheels = [ 124 | { url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, 125 | ] 126 | 127 | [[package]] 128 | name = "idna" 129 | version = "3.10" 130 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 131 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" } 132 | wheels = [ 133 | { url = "https://mirrors.aliyun.com/pypi/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" }, 134 | ] 135 | 136 | [[package]] 137 | name = "pillow" 138 | version = "11.3.0" 139 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 140 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523" } 141 | wheels = [ 142 | { url = "https://mirrors.aliyun.com/pypi/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4" }, 143 | { url = "https://mirrors.aliyun.com/pypi/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69" }, 144 | { url = "https://mirrors.aliyun.com/pypi/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d" }, 145 | { url = "https://mirrors.aliyun.com/pypi/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6" }, 146 | { url = "https://mirrors.aliyun.com/pypi/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7" }, 147 | { url = "https://mirrors.aliyun.com/pypi/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024" }, 148 | { url = "https://mirrors.aliyun.com/pypi/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809" }, 149 | { url = "https://mirrors.aliyun.com/pypi/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d" }, 150 | { url = "https://mirrors.aliyun.com/pypi/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149" }, 151 | { url = "https://mirrors.aliyun.com/pypi/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d" }, 152 | { url = "https://mirrors.aliyun.com/pypi/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542" }, 153 | { url = "https://mirrors.aliyun.com/pypi/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd" }, 154 | { url = "https://mirrors.aliyun.com/pypi/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8" }, 155 | { url = "https://mirrors.aliyun.com/pypi/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f" }, 156 | { url = "https://mirrors.aliyun.com/pypi/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c" }, 157 | { url = "https://mirrors.aliyun.com/pypi/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd" }, 158 | { url = "https://mirrors.aliyun.com/pypi/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e" }, 159 | { url = "https://mirrors.aliyun.com/pypi/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1" }, 160 | { url = "https://mirrors.aliyun.com/pypi/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805" }, 161 | { url = "https://mirrors.aliyun.com/pypi/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8" }, 162 | { url = "https://mirrors.aliyun.com/pypi/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2" }, 163 | { url = "https://mirrors.aliyun.com/pypi/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b" }, 164 | { url = "https://mirrors.aliyun.com/pypi/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3" }, 165 | { url = "https://mirrors.aliyun.com/pypi/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51" }, 166 | { url = "https://mirrors.aliyun.com/pypi/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580" }, 167 | { url = "https://mirrors.aliyun.com/pypi/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e" }, 168 | { url = "https://mirrors.aliyun.com/pypi/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d" }, 169 | { url = "https://mirrors.aliyun.com/pypi/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced" }, 170 | { url = "https://mirrors.aliyun.com/pypi/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c" }, 171 | { url = "https://mirrors.aliyun.com/pypi/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8" }, 172 | { url = "https://mirrors.aliyun.com/pypi/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59" }, 173 | { url = "https://mirrors.aliyun.com/pypi/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe" }, 174 | { url = "https://mirrors.aliyun.com/pypi/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c" }, 175 | { url = "https://mirrors.aliyun.com/pypi/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788" }, 176 | { url = "https://mirrors.aliyun.com/pypi/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31" }, 177 | { url = "https://mirrors.aliyun.com/pypi/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e" }, 178 | { url = "https://mirrors.aliyun.com/pypi/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12" }, 179 | { url = "https://mirrors.aliyun.com/pypi/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a" }, 180 | { url = "https://mirrors.aliyun.com/pypi/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632" }, 181 | { url = "https://mirrors.aliyun.com/pypi/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673" }, 182 | { url = "https://mirrors.aliyun.com/pypi/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027" }, 183 | { url = "https://mirrors.aliyun.com/pypi/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77" }, 184 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874" }, 185 | { url = "https://mirrors.aliyun.com/pypi/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a" }, 186 | { url = "https://mirrors.aliyun.com/pypi/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214" }, 187 | { url = "https://mirrors.aliyun.com/pypi/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635" }, 188 | { url = "https://mirrors.aliyun.com/pypi/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6" }, 189 | { url = "https://mirrors.aliyun.com/pypi/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae" }, 190 | { url = "https://mirrors.aliyun.com/pypi/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653" }, 191 | { url = "https://mirrors.aliyun.com/pypi/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6" }, 192 | { url = "https://mirrors.aliyun.com/pypi/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36" }, 193 | { url = "https://mirrors.aliyun.com/pypi/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b" }, 194 | { url = "https://mirrors.aliyun.com/pypi/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477" }, 195 | { url = "https://mirrors.aliyun.com/pypi/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50" }, 196 | { url = "https://mirrors.aliyun.com/pypi/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b" }, 197 | { url = "https://mirrors.aliyun.com/pypi/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12" }, 198 | { url = "https://mirrors.aliyun.com/pypi/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db" }, 199 | { url = "https://mirrors.aliyun.com/pypi/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa" }, 200 | ] 201 | 202 | [[package]] 203 | name = "pydantic" 204 | version = "2.11.7" 205 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 206 | dependencies = [ 207 | { name = "annotated-types" }, 208 | { name = "pydantic-core" }, 209 | { name = "typing-extensions" }, 210 | { name = "typing-inspection" }, 211 | ] 212 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db" } 213 | wheels = [ 214 | { url = "https://mirrors.aliyun.com/pypi/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b" }, 215 | ] 216 | 217 | [[package]] 218 | name = "pydantic-core" 219 | version = "2.33.2" 220 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 221 | dependencies = [ 222 | { name = "typing-extensions" }, 223 | ] 224 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc" } 225 | wheels = [ 226 | { url = "https://mirrors.aliyun.com/pypi/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc" }, 227 | { url = "https://mirrors.aliyun.com/pypi/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7" }, 228 | { url = "https://mirrors.aliyun.com/pypi/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025" }, 229 | { url = "https://mirrors.aliyun.com/pypi/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011" }, 230 | { url = "https://mirrors.aliyun.com/pypi/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f" }, 231 | { url = "https://mirrors.aliyun.com/pypi/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88" }, 232 | { url = "https://mirrors.aliyun.com/pypi/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1" }, 233 | { url = "https://mirrors.aliyun.com/pypi/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b" }, 234 | { url = "https://mirrors.aliyun.com/pypi/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1" }, 235 | { url = "https://mirrors.aliyun.com/pypi/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6" }, 236 | { url = "https://mirrors.aliyun.com/pypi/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea" }, 237 | { url = "https://mirrors.aliyun.com/pypi/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290" }, 238 | { url = "https://mirrors.aliyun.com/pypi/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2" }, 239 | { url = "https://mirrors.aliyun.com/pypi/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab" }, 240 | { url = "https://mirrors.aliyun.com/pypi/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f" }, 241 | { url = "https://mirrors.aliyun.com/pypi/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6" }, 242 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef" }, 243 | { url = "https://mirrors.aliyun.com/pypi/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a" }, 244 | { url = "https://mirrors.aliyun.com/pypi/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916" }, 245 | { url = "https://mirrors.aliyun.com/pypi/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a" }, 246 | { url = "https://mirrors.aliyun.com/pypi/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d" }, 247 | { url = "https://mirrors.aliyun.com/pypi/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56" }, 248 | { url = "https://mirrors.aliyun.com/pypi/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5" }, 249 | { url = "https://mirrors.aliyun.com/pypi/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e" }, 250 | { url = "https://mirrors.aliyun.com/pypi/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162" }, 251 | { url = "https://mirrors.aliyun.com/pypi/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849" }, 252 | { url = "https://mirrors.aliyun.com/pypi/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9" }, 253 | { url = "https://mirrors.aliyun.com/pypi/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9" }, 254 | { url = "https://mirrors.aliyun.com/pypi/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac" }, 255 | { url = "https://mirrors.aliyun.com/pypi/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5" }, 256 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9" }, 257 | ] 258 | 259 | [[package]] 260 | name = "sniffio" 261 | version = "1.3.1" 262 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 263 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } 264 | wheels = [ 265 | { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, 266 | ] 267 | 268 | [[package]] 269 | name = "starlette" 270 | version = "0.47.2" 271 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 272 | dependencies = [ 273 | { name = "anyio" }, 274 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 275 | ] 276 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8" } 277 | wheels = [ 278 | { url = "https://mirrors.aliyun.com/pypi/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b" }, 279 | ] 280 | 281 | [[package]] 282 | name = "typing-extensions" 283 | version = "4.14.1" 284 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 285 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36" } 286 | wheels = [ 287 | { url = "https://mirrors.aliyun.com/pypi/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" }, 288 | ] 289 | 290 | [[package]] 291 | name = "typing-inspection" 292 | version = "0.4.1" 293 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 294 | dependencies = [ 295 | { name = "typing-extensions" }, 296 | ] 297 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" } 298 | wheels = [ 299 | { url = "https://mirrors.aliyun.com/pypi/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51" }, 300 | ] 301 | 302 | [[package]] 303 | name = "uvicorn" 304 | version = "0.35.0" 305 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 306 | dependencies = [ 307 | { name = "click" }, 308 | { name = "h11" }, 309 | ] 310 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01" } 311 | wheels = [ 312 | { url = "https://mirrors.aliyun.com/pypi/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a" }, 313 | ] 314 | --------------------------------------------------------------------------------