├── .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 |
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 |
--------------------------------------------------------------------------------