gcj-rectify 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: gcj-rectify
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: fastapi>=0.116.1
8
+ Requires-Dist: httpx>=0.28.1
9
+ Requires-Dist: pillow>=11.3.0
10
+ Requires-Dist: uvicorn>=0.35.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # gcj-rectify
14
+
15
+
16
+ ```bash
17
+ uv run uvicorn gcj_rectify_server.main:app --reload
18
+ ```
@@ -0,0 +1,12 @@
1
+ gcj_rectify_server/__init__.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
2
+ gcj_rectify_server/fetch.py,sha256=EUMcJR9HDnolHHT3qrPAuOXr81GbdnUKojhhvwt0kuQ,2243
3
+ gcj_rectify_server/main.py,sha256=qGA5_6Q94L18syCKGGdX0AF6OQcF8lyjJU8AFU-vl1k,2768
4
+ gcj_rectify_server/maps.json,sha256=ztMPq4PaPgMUE9DqxlOM_Qv6tws_Q8L_XzwPZu3Khh4,520
5
+ gcj_rectify_server/rectify.py,sha256=WdMCTBKokniqn3RNprMIneCm-I8-5i7nXvIbPAkTx7s,6547
6
+ gcj_rectify_server/transform.py,sha256=pB4ICCAxY7E503RHB0zMhdeUcce1J7JkgtmFiOAJQT0,4927
7
+ gcj_rectify_server/utils.py,sha256=qzd12AFRxAf_C0O7SjhHfzzbXk6SySiiFxTs-KuDXCM,3568
8
+ gcj_rectify-0.1.0.dist-info/METADATA,sha256=epKL0BfvsftNtWiainAYiJIjqkSqE92Qh1Usm3b_Tas,378
9
+ gcj_rectify-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ gcj_rectify-0.1.0.dist-info/entry_points.txt,sha256=KnPECyS9pEuDVsb5_0QbrX0sIdK8FLYfqX7npk1-qws,60
11
+ gcj_rectify-0.1.0.dist-info/licenses/LICENSE,sha256=LTlWKOboZ-3SJpK_6wg152Q-3W1GV2-HeAtRkNLjgaI,1086
12
+ gcj_rectify-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gcj_rectify = gcj_rectify_server.main:run
@@ -0,0 +1,21 @@
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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,70 @@
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
@@ -0,0 +1,89 @@
1
+ from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
+ import argparse
4
+
5
+ from fastapi import FastAPI, Response, Request
6
+ import uvicorn
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
11
+
12
+
13
+ @asynccontextmanager
14
+ async def lifespan(app: FastAPI):
15
+ """应用生命周期管理"""
16
+ # 启动时执行
17
+ # 在启动服务器前重置异步客户端,确保使用新的事件循环
18
+ reset_async_client()
19
+ yield
20
+ # 关闭时执行
21
+ from .fetch import close_async_client_async
22
+
23
+ await close_async_client_async()
24
+
25
+
26
+ app = FastAPI(lifespan=lifespan)
27
+
28
+
29
+ app.state.cache_dir = Path(get_cache_dir())
30
+
31
+
32
+ @app.get("/")
33
+ def index():
34
+ return {"message": "Server is Running"}
35
+
36
+
37
+ @app.get("/config")
38
+ def get_config(request: Request):
39
+ return {"cache_dir": str(request.app.state.cache_dir)}
40
+
41
+
42
+ @app.get("/tiles/{map_id}/{z}/{x}/{y}")
43
+ async def tile(map_id: str, z: int, x: int, y: int, request: Request):
44
+ """
45
+ Get a tile image for the specified map ID, zoom level, and row/column numbers.
46
+
47
+ Args:
48
+ map_id (str): The ID of the map.
49
+ z (int): Zoom level.
50
+ x (int): Tile column number.
51
+ y (int): Tile row number.
52
+ request: Fastapi Request
53
+ """
54
+ state_cache_dir = request.app.state.cache_dir
55
+ try:
56
+ if z <= 9:
57
+ # For zoom levels 9 and below, use GCJ02 tiles directly
58
+ img_bytes = await get_tile_gcj_cached(x, y, z, map_id, state_cache_dir)
59
+ else:
60
+ img_bytes = await get_tile_wgs_cached(x, y, z, map_id, state_cache_dir)
61
+
62
+ if img_bytes is None:
63
+ # 如果获取瓦片失败,返回空图片或错误响应
64
+ return Response(status_code=500, content="Failed to fetch tile")
65
+
66
+ return Response(content=img_bytes, media_type="image/png")
67
+ except Exception as e:
68
+ print(f"获取瓦片时发生错误: {e}")
69
+ return Response(status_code=500, content="Internal server error")
70
+
71
+
72
+ def run(host: str = "0.0.0.0", port: int = 8000):
73
+ """运行 GCJ Rectify 服务器
74
+
75
+ Args:
76
+ host: 服务器主机地址,默认为0.0.0.0
77
+ port: 服务器端口,默认为8000
78
+ """
79
+ # 解析命令行参数
80
+ parser = argparse.ArgumentParser(description="GCJ Rectify 服务器")
81
+ parser.add_argument("--host", default=host, help="服务器主机地址 (默认: 0.0.0.0)")
82
+ parser.add_argument(
83
+ "--port", type=int, default=port, help="服务器端口 (默认: 8000)"
84
+ )
85
+
86
+ args = parser.parse_args()
87
+
88
+ print(f"启动服务器: http://{args.host}:{args.port}")
89
+ uvicorn.run(app, host=args.host, port=args.port)
@@ -0,0 +1,14 @@
1
+ {
2
+ "amap-vec": {
3
+ "name": "高德地图 - 矢量地图",
4
+ "url": "https://wprd02.is.autonavi.com//appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}"
5
+ },
6
+ "amap-sat": {
7
+ "name": "高德地图 - 卫星影像",
8
+ "url": "https://wprd02.is.autonavi.com//appmaptile?lang=zh_cn&size=1&scale=1&style=6&x={x}&y={y}&z={z}"
9
+ },
10
+ "tencent-vec": {
11
+ "name": "腾讯地图 - 矢量地图",
12
+ "url": "http://rt0.map.gtimg.com/realtimerender?z={z}&x={x}&y={-y}&type=vector&style=0"
13
+ }
14
+ }
@@ -0,0 +1,200 @@
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
+ map_data = get_maps()
18
+
19
+
20
+ async def get_tile_gcj(x: int, y: int, z: int, mapid: str) -> bytes:
21
+ """
22
+ 获取指定瓦片的图像,这里下载的是原始瓦片(GCJ02 坐标系)。
23
+ Args:
24
+ x (int): Tile X coordinate.
25
+ y (int): Tile Y coordinate.
26
+ z (int): Zoom level.
27
+ mapid (str): Map Id
28
+ Returns:
29
+ bytes: Tile image bytes.
30
+ """
31
+ url = map_data[mapid]["url"]
32
+ if "-y" in url:
33
+ # 如果 URL 中包含 -y,认为是TMS格式,需要调整 Y 值
34
+ url = url.replace("-y", "y")
35
+ url = url.format(x=x, y=(2**z - 1 - y), z=z)
36
+ else:
37
+ url = url.format(x=x, y=y, z=z)
38
+
39
+ # 使用异步HTTP客户端获取瓦片
40
+ content = await fetch_tile(url)
41
+
42
+ return content
43
+
44
+
45
+ async def get_tile_gcj_cached(
46
+ x: int, y: int, z: int, mapid: str, cache_dir: Path = Path.cwd().joinpath("cache")
47
+ ) -> Image:
48
+ """
49
+ 获取指定瓦片的图像,使用缓存。
50
+ Args:
51
+ x (int): Tile X coordinate.
52
+ y (int): Tile Y coordinate.
53
+ z (int): Zoom level.
54
+ mapid (str): Map Id
55
+ cache_dir (str): 缓存目录
56
+ Returns:
57
+ Image: Tile image.
58
+ """
59
+ tile_file_path = cache_dir.joinpath(f"{mapid}/GCJ/{z}/{x}/{y}.png")
60
+ if tile_file_path.exists():
61
+ # print(f"从缓存中获取了瓦片: {tile_file_path}")
62
+ image = Image.open(tile_file_path)
63
+ return image_to_bytes(image)
64
+ # 如果缓存不存在,则下载瓦片并保存到缓存
65
+ tile_file_path.parent.mkdir(parents=True, exist_ok=True)
66
+ try:
67
+ image_bytes = await get_tile_gcj(x, y, z, mapid)
68
+ image = bytes_to_image(image_bytes)
69
+ image.save(tile_file_path, "PNG")
70
+ except Exception as e:
71
+ print(f"获取瓦片失败: {e}")
72
+ # 如果是事件循环错误,尝试重置客户端并重试
73
+ if "Event loop is closed" in str(e) or "RuntimeError" in str(e):
74
+ from .fetch import reset_async_client
75
+
76
+ reset_async_client()
77
+ try:
78
+ image_bytes = await get_tile_gcj(x, y, z, mapid)
79
+ image = bytes_to_image(image_bytes)
80
+ image.save(tile_file_path, "PNG")
81
+ except Exception as retry_e:
82
+ print(f"重试获取瓦片失败: {retry_e}")
83
+ return None
84
+ else:
85
+ return None
86
+
87
+ return image_bytes
88
+
89
+
90
+ async def get_tile_wgs(x: int, y: int, z: int, mapid: str) -> Image:
91
+ """
92
+ 获取瓦片(调整为 WGS84 坐标系)
93
+ """
94
+ if z <= 9:
95
+ print("Z 小于 9 时 没有明显的偏移 直接使用 GCJ02 坐标系的瓦片")
96
+ return
97
+
98
+ wgs_bbox = xyz_to_bbox(x, y, z)
99
+ gcj_bbox = wgsbbox_to_gcjbbox(wgs_bbox)
100
+ left_upper, right_lower = gcj_bbox
101
+
102
+ # 计算左上角和右下角的瓦片行列号
103
+ x_min, y_min = lonlat_to_xyz(left_upper[0], left_upper[1], z) # 左上角
104
+ x_max, y_max = lonlat_to_xyz(right_lower[0], right_lower[1], z) # 右下角
105
+
106
+ # 创建任务列表,异步获取所有需要的瓦片
107
+ tasks = []
108
+ for ax in range(x_min, x_max + 1):
109
+ for ay in range(y_min, y_max + 1):
110
+ tasks.append(get_tile_gcj_cached(ax, ay, z, mapid))
111
+
112
+ # 并发执行所有瓦片下载任务
113
+ tiles = await asyncio.gather(*tasks)
114
+ tile_images = [Image.open(BytesIO(content)) for content in tiles]
115
+
116
+ # 拼合瓦片
117
+ composite = Image.new(
118
+ "RGBA", ((x_max - x_min + 1) * 256, (y_max - y_min + 1) * 256)
119
+ )
120
+
121
+ tile_index = 0
122
+ for i, ax in enumerate(range(x_min, x_max + 1)):
123
+ for j, ay in enumerate(range(y_min, y_max + 1)):
124
+ tile = tile_images[tile_index]
125
+ if tile:
126
+ composite.paste(tile, (i * 256, j * 256))
127
+ tile_index += 1
128
+
129
+ # 计算拼合后的瓦片范围
130
+ megred_bbox = xyz_to_bbox(x_min, y_min, z)[0], xyz_to_bbox(x_max, y_max, z)[1]
131
+
132
+ x_range = megred_bbox[1][0] - megred_bbox[0][0]
133
+ y_range = megred_bbox[0][1] - megred_bbox[1][1]
134
+
135
+ left_percent = (gcj_bbox[0][0] - megred_bbox[0][0]) / x_range
136
+ top_percent = (megred_bbox[0][1] - gcj_bbox[0][1]) / y_range
137
+ img_width, img_height = composite.size
138
+ # 裁剪选区(left, top, right, bottom)
139
+ crop_bbox = (
140
+ int(left_percent * img_width),
141
+ int(top_percent * img_height),
142
+ int(left_percent * img_width) + 256,
143
+ int(top_percent * img_height) + 256,
144
+ )
145
+
146
+ # 从拼合的瓦片中裁剪出对应的区域
147
+ croped_image = composite.crop(crop_bbox)
148
+ return image_to_bytes(croped_image)
149
+
150
+
151
+ async def get_tile_wgs_cached(
152
+ x: int, y: int, z: int, mapid: str, cache_dir: Path = Path.cwd().joinpath("cache")
153
+ ) -> Image:
154
+ """
155
+ 获取指定瓦片的图像,使用缓存。
156
+ Args:
157
+ x (int): Tile X coordinate.
158
+ y (int): Tile Y coordinate.
159
+ z (int): Zoom level.
160
+ mapid (str): Map Id
161
+ cache_dir (str): 缓存目录
162
+ Returns:
163
+ Image: Tile image.
164
+ """
165
+ tile_file_path = cache_dir.joinpath(f"{mapid}/WGS/{z}/{x}/{y}.png")
166
+ if tile_file_path.exists():
167
+ # print(f"从缓存中获取了瓦片: {tile_file_path}")
168
+ image = Image.open(tile_file_path)
169
+ return image_to_bytes(image)
170
+ # 如果缓存不存在,则下载瓦片并保存到缓存
171
+ tile_file_path.parent.mkdir(parents=True, exist_ok=True)
172
+ try:
173
+ image_bytes = await get_tile_wgs(x, y, z, mapid)
174
+ image = bytes_to_image(image_bytes)
175
+ image.save(tile_file_path, "PNG")
176
+ except Exception as e:
177
+ print(f"获取WGS瓦片失败: {e}")
178
+ # 如果是事件循环错误,尝试重置客户端并重试
179
+ if "Event loop is closed" in str(e) or "RuntimeError" in str(e):
180
+ from .fetch import reset_async_client
181
+
182
+ reset_async_client()
183
+ try:
184
+ image_bytes = await get_tile_wgs(x, y, z, mapid)
185
+ image = bytes_to_image(image_bytes)
186
+ image.save(tile_file_path, "PNG")
187
+ except Exception as retry_e:
188
+ print(f"重试获取WGS瓦片失败: {retry_e}")
189
+ return None
190
+ else:
191
+ return None
192
+
193
+ return image_bytes
194
+
195
+
196
+ # 测试
197
+
198
+
199
+ # http://127.0.0.1:8000/tiles/amap-sat/11/1661/807
200
+ # https://wprd02.is.autonavi.com//appmaptile?lang=zh_cn&size=1&scale=1&style=6&x=1661&y=807&z=11
@@ -0,0 +1,140 @@
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
@@ -0,0 +1,134 @@
1
+ import json
2
+ from io import BytesIO
3
+ import os
4
+ from math import atan, cos, log, pi, sinh, tan
5
+ from pathlib import Path
6
+
7
+ from PIL import Image
8
+
9
+ from .transform import wgs2gcj
10
+
11
+ APP_DIR = Path(__file__).parent
12
+
13
+
14
+ def get_cache_dir() -> str:
15
+ """
16
+ Get the cache directory from the environment variable or default to the app directory.
17
+
18
+ Returns:
19
+ str: The path to the cache directory.
20
+ """
21
+ env_cache = os.getenv("GCJRE_CACHE", "")
22
+ if env_cache:
23
+ print(f"Using cache directory from environment: {env_cache}")
24
+ return env_cache
25
+ print(f"Using current directory for cache: {Path.cwd().joinpath('cache')}")
26
+ return str(Path.cwd().joinpath("cache"))
27
+
28
+
29
+ def get_maps():
30
+ return json.load(open(str(APP_DIR.joinpath("maps.json")), "r", encoding="utf-8"))
31
+
32
+
33
+ def bytes_to_image(content: bytes) -> Image:
34
+ """
35
+ Convert bytes to a PIL Image.
36
+
37
+ Args:
38
+ content (bytes): Image data in bytes.
39
+
40
+ Returns:
41
+ Image: PIL Image object.
42
+ """
43
+ return Image.open(BytesIO(content))
44
+
45
+
46
+ def image_to_bytes(image: Image, format: str = "PNG") -> bytes:
47
+ """
48
+ Convert a PIL Image to bytes.
49
+
50
+ Args:
51
+ image (Image): PIL Image object.
52
+ format (str): Format to save the image, default is "PNG".
53
+
54
+ Returns:
55
+ bytes: Image data in bytes.
56
+ """
57
+ img_buffer = BytesIO()
58
+ image.save(img_buffer, format=format)
59
+ img_bytes = img_buffer.getvalue()
60
+ img_buffer.close()
61
+ return img_bytes
62
+
63
+
64
+ def xyz_to_lonlat(x: int, y: int, z: int) -> tuple:
65
+ """
66
+ 将XYZ瓦片坐标转换为经纬度(左上角点)。
67
+
68
+ Args:
69
+ x (int): Tile X coordinate.
70
+ y (int): Tile Y coordinate.
71
+ z (int): Zoom level.
72
+
73
+ Returns:
74
+ tuple: Longitude and latitude in degrees.
75
+ """
76
+ n = 2.0**z
77
+ lon_deg = x / n * 360.0 - 180.0
78
+ lat_rad = atan(sinh(pi * (1 - 2 * y / n)))
79
+ lat_deg = lat_rad * 180.0 / pi
80
+ return lon_deg, lat_deg
81
+
82
+
83
+ def lonlat_to_xyz(lon: float, lat: float, z: int) -> tuple:
84
+ """
85
+ Convert longitude and latitude to XYZ tile coordinates.
86
+
87
+ Args:
88
+ lon (float): Longitude in degrees.
89
+ lat (float): Latitude in degrees.
90
+ z (int): Zoom level.
91
+
92
+ Returns:
93
+ tuple: Tile X and Y coordinates.
94
+ """
95
+ n = 2.0**z
96
+ x = (lon + 180.0) / 360.0 * n
97
+ lat_rad = lat * pi / 180.0
98
+ t = log(tan(lat_rad) + 1 / cos(lat_rad))
99
+ y = (1 - t / pi) * n / 2
100
+ return int(x), int(y)
101
+
102
+
103
+ def xyz_to_bbox(x, y, z):
104
+ """
105
+ Convert XYZ tile coordinates to bounding box coordinates.
106
+
107
+ Args:
108
+ x (int): Tile X coordinate.
109
+ y (int): Tile Y coordinate.
110
+ z (int): Zoom level.
111
+
112
+ Returns:
113
+ tuple: Bounding box in the format (min_lon, min_lat, max_lon, max_lat).
114
+ """
115
+ left_upper_lon, left_upper_lat = xyz_to_lonlat(x, y, z)
116
+ right_lower_lon, right_lower_lat = xyz_to_lonlat(x + 1, y + 1, z)
117
+
118
+ return (left_upper_lon, left_upper_lat), (right_lower_lon, right_lower_lat)
119
+
120
+
121
+ def wgsbbox_to_gcjbbox(wgs_bbox):
122
+ """
123
+ Convert WGS84 bounding box to GCJ02 bounding box.
124
+
125
+ Args:
126
+ wgs_bbox (tuple): Bounding box in the format (min_lon, min_lat, max_lon, max_lat).
127
+
128
+ Returns:
129
+ tuple: GCJ02 bounding box in the same format.
130
+ """
131
+ left_upper, right_lower = wgs_bbox
132
+ gcj_left_upper = wgs2gcj(left_upper[0], left_upper[1])
133
+ gcj_right_lower = wgs2gcj(right_lower[0], right_lower[1])
134
+ return gcj_left_upper, gcj_right_lower