cityposter 0.1.0__tar.gz
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.
- cityposter-0.1.0/LICENSE +22 -0
- cityposter-0.1.0/PKG-INFO +169 -0
- cityposter-0.1.0/README.md +125 -0
- cityposter-0.1.0/cityposter/__init__.py +0 -0
- cityposter-0.1.0/cityposter/cli.py +353 -0
- cityposter-0.1.0/cityposter/config.py +80 -0
- cityposter-0.1.0/cityposter/config_poi.py +128 -0
- cityposter-0.1.0/cityposter/coordinate_systems.py +97 -0
- cityposter-0.1.0/cityposter/data/__init__.py +0 -0
- cityposter-0.1.0/cityposter/data/cache.py +102 -0
- cityposter-0.1.0/cityposter/data/fetcher.py +115 -0
- cityposter-0.1.0/cityposter/data/sinogdb.py +448 -0
- cityposter-0.1.0/cityposter/fonts/__init__.py +0 -0
- cityposter-0.1.0/cityposter/fonts/manager.py +163 -0
- cityposter-0.1.0/cityposter/geocoding.py +166 -0
- cityposter-0.1.0/cityposter/poi/__init__.py +0 -0
- cityposter-0.1.0/cityposter/poi/categories.py +189 -0
- cityposter-0.1.0/cityposter/rendering/__init__.py +0 -0
- cityposter-0.1.0/cityposter/rendering/gradients.py +54 -0
- cityposter-0.1.0/cityposter/rendering/layers.py +239 -0
- cityposter-0.1.0/cityposter/rendering/poi.py +368 -0
- cityposter-0.1.0/cityposter/rendering/poster.py +499 -0
- cityposter-0.1.0/cityposter/rendering/typography.py +96 -0
- cityposter-0.1.0/cityposter/themes/__init__.py +0 -0
- cityposter-0.1.0/cityposter/themes/manager.py +105 -0
- cityposter-0.1.0/cityposter.egg-info/PKG-INFO +169 -0
- cityposter-0.1.0/cityposter.egg-info/SOURCES.txt +31 -0
- cityposter-0.1.0/cityposter.egg-info/dependency_links.txt +1 -0
- cityposter-0.1.0/cityposter.egg-info/entry_points.txt +2 -0
- cityposter-0.1.0/cityposter.egg-info/requires.txt +32 -0
- cityposter-0.1.0/cityposter.egg-info/top_level.txt +1 -0
- cityposter-0.1.0/pyproject.toml +74 -0
- cityposter-0.1.0/setup.cfg +4 -0
cityposter-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Ankur Gupta (original maptoposter)
|
|
4
|
+
Copyright (c) 2026 cityposter contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cityposter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate beautiful, minimalist map posters for any city — now with offline SinoGDB support for Chinese cities.
|
|
5
|
+
Author-email: Ankur Gupta <originalankur@github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: map,poster,osm,openstreetmap,art,visualization,china,sinogdb
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: aiohttp>=3.14.1
|
|
12
|
+
Requires-Dist: certifi==2026.1.4
|
|
13
|
+
Requires-Dist: charset-normalizer==3.4.4
|
|
14
|
+
Requires-Dist: contourpy==1.3.3
|
|
15
|
+
Requires-Dist: cycler==0.12.1
|
|
16
|
+
Requires-Dist: fonttools==4.61.1
|
|
17
|
+
Requires-Dist: geocode-amap>=0.1.0
|
|
18
|
+
Requires-Dist: geographiclib==2.1
|
|
19
|
+
Requires-Dist: geopandas==1.1.2
|
|
20
|
+
Requires-Dist: geopy==2.4.1
|
|
21
|
+
Requires-Dist: idna==3.11
|
|
22
|
+
Requires-Dist: kiwisolver==1.4.9
|
|
23
|
+
Requires-Dist: lat-lon-parser==1.3.1
|
|
24
|
+
Requires-Dist: matplotlib==3.10.8
|
|
25
|
+
Requires-Dist: networkx==3.6.1
|
|
26
|
+
Requires-Dist: numpy==2.4.0
|
|
27
|
+
Requires-Dist: osmnx==2.0.7
|
|
28
|
+
Requires-Dist: packaging==25.0
|
|
29
|
+
Requires-Dist: pandas==2.3.3
|
|
30
|
+
Requires-Dist: pillow==12.1.0
|
|
31
|
+
Requires-Dist: pyogrio==0.12.1
|
|
32
|
+
Requires-Dist: pyparsing==3.3.1
|
|
33
|
+
Requires-Dist: pyproj==3.7.2
|
|
34
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
35
|
+
Requires-Dist: pytz==2025.2
|
|
36
|
+
Requires-Dist: requests==2.32.5
|
|
37
|
+
Requires-Dist: scipy==1.16.3
|
|
38
|
+
Requires-Dist: shapely==2.1.2
|
|
39
|
+
Requires-Dist: six==1.17.0
|
|
40
|
+
Requires-Dist: tqdm==4.67.1
|
|
41
|
+
Requires-Dist: tzdata==2025.3
|
|
42
|
+
Requires-Dist: urllib3==2.6.3
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# CityPoster — 城市地图海报生成器
|
|
46
|
+
|
|
47
|
+
基于 [maptoposter](https://github.com/originalankur/maptoposter) 改造,专注中国城市离线地图海报。
|
|
48
|
+
|
|
49
|
+
## 安装
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/songwupei/cityposter.git
|
|
53
|
+
cd cityposter
|
|
54
|
+
uv sync
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### SinoGDB 数据 (中国城市离线生成)
|
|
58
|
+
|
|
59
|
+
下载 [SinoGDB-PackSet](https://docs.qq.com/smartsheet/DSHBNQ1FUTEZJek9q),设置路径:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export SINOGDB_DIR="$HOME/Downloads/SinoGDB-PackSet_gpkg_20260208"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 使用
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 中国城市 — 完全离线,秒级出图
|
|
69
|
+
cityposter --lat 39.9042 --lon 116.4074 -dc "北京" -dC "中国" -t noir --poi
|
|
70
|
+
cityposter --lat 30.5728 --lon 104.0668 -dc "成都" -dC "中国" -t gongqiang --poi
|
|
71
|
+
|
|
72
|
+
# 国际城市 — 走 OSMnx
|
|
73
|
+
cityposter -c "Paris" -C "France" -t pastel_dream -d 10000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 参数
|
|
77
|
+
|
|
78
|
+
| 参数 | 说明 | 默认 |
|
|
79
|
+
|------|------|------|
|
|
80
|
+
| `--lat`, `--lon` | 中心坐标 (WGS-84) | |
|
|
81
|
+
| `--gcj02` | 坐标视为 GCJ-02 | |
|
|
82
|
+
| `-dc`, `-dC` | 显示名称 | |
|
|
83
|
+
| `-t` | 主题 | terracotta |
|
|
84
|
+
| `-d` | 半径 (米) | 8000(中国) |
|
|
85
|
+
| `--poi` | POI 标注 | 关闭 |
|
|
86
|
+
| | 开启后显示:地铁站、火车站、景点、学校、医院等 | |
|
|
87
|
+
|
|
88
|
+
### `--poi` 详情
|
|
89
|
+
|
|
90
|
+
开启后从 SinoGDB 拉取以下数据并标注(最多 50 个标记、15 个标签):
|
|
91
|
+
|
|
92
|
+
| 数据源 | 类型 | 标记 |
|
|
93
|
+
|--------|------|------|
|
|
94
|
+
| `railway_point` | 地铁站 | ○ 空心圆环 + 站名 |
|
|
95
|
+
| | 火车站 | ■ 方块 |
|
|
96
|
+
| `transport_point` | 公交站/机场/港口 | ●/◆ 按类型 |
|
|
97
|
+
| `natural_point` | 山峰/洞穴/温泉 | ▲/★ |
|
|
98
|
+
| `landuse` (学校/医院/寺庙) | 学校/医院/寺庙 | ●/■/▲ |
|
|
99
|
+
|
|
100
|
+
不加 `--poi` 则不显示任何标注,纯地图。
|
|
101
|
+
|
|
102
|
+
### 自定义 POI
|
|
103
|
+
|
|
104
|
+
在 `config/custom-poi/` 放置 JSON 文件,`--poi` 时自动加载:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
[
|
|
108
|
+
{"name": "故宫", "lat": 39.9163, "lon": 116.3972, "category": "landmark"},
|
|
109
|
+
{"name": "天安门", "lat": 39.9087, "lon": 116.3975, "category": "sightseeing_spot"}
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
支持 category: `landmark`, `sightseeing_spot`, `museum`, `park`, `temple`, `school`, `cafe`, `mall` 等。
|
|
114
|
+
|
|
115
|
+
| `-W`, `-H` | 画布 (英寸) | 12x16 |
|
|
116
|
+
| `-f` | png/svg | png |
|
|
117
|
+
| `--font-family` | Google Fonts | Noto Sans SC(中国) |
|
|
118
|
+
|
|
119
|
+
### 所有主题 (41)
|
|
120
|
+
|
|
121
|
+
| 主题 | 名称 | 风格 |
|
|
122
|
+
|------|------|------|
|
|
123
|
+
| `autumn` | Autumn | 秋日橙红 |
|
|
124
|
+
| `blueprint` | Blueprint | 建筑蓝图 |
|
|
125
|
+
| `brutalist_concrete` | Brutalist Concrete | 粗野主义 |
|
|
126
|
+
| `carbon_fiber` | Carbon Fiber | 碳纤维黑 |
|
|
127
|
+
| `contrast_zones` | Contrast Zones | 高对比度 |
|
|
128
|
+
| `copper_patina` | Copper Patina | 铜绿氧化 |
|
|
129
|
+
| `cotton_candy` | Cotton Candy | 棉花糖粉紫 |
|
|
130
|
+
| `cyberpunk_neon` | Cyberpunk Neon | 赛博朋克 |
|
|
131
|
+
| `desert_rose` | Desert Rose | 沙漠玫瑰 |
|
|
132
|
+
| `emerald` | Emerald City | 翡翠绿 |
|
|
133
|
+
| `forest` | Forest | 森林绿 |
|
|
134
|
+
| `forest_moss` | Forest Moss | 苔藓绿金 |
|
|
135
|
+
| `gilded_noir` | Gilded Noir | 黑金奢华 |
|
|
136
|
+
| `glitch_purple` | Glitch Purple | 故障紫绿 |
|
|
137
|
+
| `gongqiang` | 宫墙 Gongqiang | 红底鎏金 |
|
|
138
|
+
| `gradient_roads` | Gradient Roads | 渐变道路 |
|
|
139
|
+
| `japanese_ink` | Japanese Ink | 日式水墨 |
|
|
140
|
+
| `lavender_mist` | Lavender Mist | 薰衣草雾 |
|
|
141
|
+
| `matcha_latte` | Matcha Latte | 抹茶拿铁 |
|
|
142
|
+
| `mediterranean_summer` | Mediterranean Summer | 地中海蓝橙 |
|
|
143
|
+
| `mediterranean_summer_plus` | Mediterranean Summer + Rail | 地中海+铁路 |
|
|
144
|
+
| `midnight_blue` | Midnight Blue | 午夜蓝金 |
|
|
145
|
+
| `monochrome_blue` | Monochrome Blue | 单色蓝 |
|
|
146
|
+
| `neon_cyberpunk` | Neon Cyberpunk | 霓虹赛博朋克 |
|
|
147
|
+
| `noir` | Noir | 纯黑白 |
|
|
148
|
+
| `nordic_frost` | Nordic Frost | 北欧冰霜 |
|
|
149
|
+
| `ocean` | Ocean | 海洋蓝 |
|
|
150
|
+
| `ocean_abyss` | Ocean Abyss | 深海深渊 |
|
|
151
|
+
| `pastel_dream` | Pastel Dream | 梦幻粉彩 |
|
|
152
|
+
| `qingzhuan` | 青砖 Qingzhuan | 胡同灰砖 |
|
|
153
|
+
| `red_alert` | Red Alert | 红色警报 |
|
|
154
|
+
| `royal_velvet` | Royal Velvet | 皇家天鹅绒 |
|
|
155
|
+
| `sakura_branch` | Sakura Branch | 樱花粉 |
|
|
156
|
+
| `shuimo` | 水墨 Shuimo | 水墨白底 |
|
|
157
|
+
| `solarized_dark` | Solarized Dark | Solarized 暗色 |
|
|
158
|
+
| `sulfur_slate` | Sulfur & Slate | 硫磺金灰 |
|
|
159
|
+
| `sunset` | Sunset | 落日橙粉 |
|
|
160
|
+
| `terra_clay` | Terra Clay | 陶土暖色 |
|
|
161
|
+
| `terracotta` | Terracotta | 地中海陶 |
|
|
162
|
+
| `vintage_nautical` | Vintage Nautical | 复古航海 |
|
|
163
|
+
| `warm_beige` | Warm Beige | 暖米色 |
|
|
164
|
+
|
|
165
|
+
## Credit
|
|
166
|
+
|
|
167
|
+
Forked from [maptoposter](https://github.com/originalankur/maptoposter) by Ankur Gupta. MIT License.
|
|
168
|
+
|
|
169
|
+
SinoGDB data derived from OpenStreetMap, processed by QGIS.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# CityPoster — 城市地图海报生成器
|
|
2
|
+
|
|
3
|
+
基于 [maptoposter](https://github.com/originalankur/maptoposter) 改造,专注中国城市离线地图海报。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/songwupei/cityposter.git
|
|
9
|
+
cd cityposter
|
|
10
|
+
uv sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### SinoGDB 数据 (中国城市离线生成)
|
|
14
|
+
|
|
15
|
+
下载 [SinoGDB-PackSet](https://docs.qq.com/smartsheet/DSHBNQ1FUTEZJek9q),设置路径:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export SINOGDB_DIR="$HOME/Downloads/SinoGDB-PackSet_gpkg_20260208"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 使用
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 中国城市 — 完全离线,秒级出图
|
|
25
|
+
cityposter --lat 39.9042 --lon 116.4074 -dc "北京" -dC "中国" -t noir --poi
|
|
26
|
+
cityposter --lat 30.5728 --lon 104.0668 -dc "成都" -dC "中国" -t gongqiang --poi
|
|
27
|
+
|
|
28
|
+
# 国际城市 — 走 OSMnx
|
|
29
|
+
cityposter -c "Paris" -C "France" -t pastel_dream -d 10000
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 参数
|
|
33
|
+
|
|
34
|
+
| 参数 | 说明 | 默认 |
|
|
35
|
+
|------|------|------|
|
|
36
|
+
| `--lat`, `--lon` | 中心坐标 (WGS-84) | |
|
|
37
|
+
| `--gcj02` | 坐标视为 GCJ-02 | |
|
|
38
|
+
| `-dc`, `-dC` | 显示名称 | |
|
|
39
|
+
| `-t` | 主题 | terracotta |
|
|
40
|
+
| `-d` | 半径 (米) | 8000(中国) |
|
|
41
|
+
| `--poi` | POI 标注 | 关闭 |
|
|
42
|
+
| | 开启后显示:地铁站、火车站、景点、学校、医院等 | |
|
|
43
|
+
|
|
44
|
+
### `--poi` 详情
|
|
45
|
+
|
|
46
|
+
开启后从 SinoGDB 拉取以下数据并标注(最多 50 个标记、15 个标签):
|
|
47
|
+
|
|
48
|
+
| 数据源 | 类型 | 标记 |
|
|
49
|
+
|--------|------|------|
|
|
50
|
+
| `railway_point` | 地铁站 | ○ 空心圆环 + 站名 |
|
|
51
|
+
| | 火车站 | ■ 方块 |
|
|
52
|
+
| `transport_point` | 公交站/机场/港口 | ●/◆ 按类型 |
|
|
53
|
+
| `natural_point` | 山峰/洞穴/温泉 | ▲/★ |
|
|
54
|
+
| `landuse` (学校/医院/寺庙) | 学校/医院/寺庙 | ●/■/▲ |
|
|
55
|
+
|
|
56
|
+
不加 `--poi` 则不显示任何标注,纯地图。
|
|
57
|
+
|
|
58
|
+
### 自定义 POI
|
|
59
|
+
|
|
60
|
+
在 `config/custom-poi/` 放置 JSON 文件,`--poi` 时自动加载:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
[
|
|
64
|
+
{"name": "故宫", "lat": 39.9163, "lon": 116.3972, "category": "landmark"},
|
|
65
|
+
{"name": "天安门", "lat": 39.9087, "lon": 116.3975, "category": "sightseeing_spot"}
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
支持 category: `landmark`, `sightseeing_spot`, `museum`, `park`, `temple`, `school`, `cafe`, `mall` 等。
|
|
70
|
+
|
|
71
|
+
| `-W`, `-H` | 画布 (英寸) | 12x16 |
|
|
72
|
+
| `-f` | png/svg | png |
|
|
73
|
+
| `--font-family` | Google Fonts | Noto Sans SC(中国) |
|
|
74
|
+
|
|
75
|
+
### 所有主题 (41)
|
|
76
|
+
|
|
77
|
+
| 主题 | 名称 | 风格 |
|
|
78
|
+
|------|------|------|
|
|
79
|
+
| `autumn` | Autumn | 秋日橙红 |
|
|
80
|
+
| `blueprint` | Blueprint | 建筑蓝图 |
|
|
81
|
+
| `brutalist_concrete` | Brutalist Concrete | 粗野主义 |
|
|
82
|
+
| `carbon_fiber` | Carbon Fiber | 碳纤维黑 |
|
|
83
|
+
| `contrast_zones` | Contrast Zones | 高对比度 |
|
|
84
|
+
| `copper_patina` | Copper Patina | 铜绿氧化 |
|
|
85
|
+
| `cotton_candy` | Cotton Candy | 棉花糖粉紫 |
|
|
86
|
+
| `cyberpunk_neon` | Cyberpunk Neon | 赛博朋克 |
|
|
87
|
+
| `desert_rose` | Desert Rose | 沙漠玫瑰 |
|
|
88
|
+
| `emerald` | Emerald City | 翡翠绿 |
|
|
89
|
+
| `forest` | Forest | 森林绿 |
|
|
90
|
+
| `forest_moss` | Forest Moss | 苔藓绿金 |
|
|
91
|
+
| `gilded_noir` | Gilded Noir | 黑金奢华 |
|
|
92
|
+
| `glitch_purple` | Glitch Purple | 故障紫绿 |
|
|
93
|
+
| `gongqiang` | 宫墙 Gongqiang | 红底鎏金 |
|
|
94
|
+
| `gradient_roads` | Gradient Roads | 渐变道路 |
|
|
95
|
+
| `japanese_ink` | Japanese Ink | 日式水墨 |
|
|
96
|
+
| `lavender_mist` | Lavender Mist | 薰衣草雾 |
|
|
97
|
+
| `matcha_latte` | Matcha Latte | 抹茶拿铁 |
|
|
98
|
+
| `mediterranean_summer` | Mediterranean Summer | 地中海蓝橙 |
|
|
99
|
+
| `mediterranean_summer_plus` | Mediterranean Summer + Rail | 地中海+铁路 |
|
|
100
|
+
| `midnight_blue` | Midnight Blue | 午夜蓝金 |
|
|
101
|
+
| `monochrome_blue` | Monochrome Blue | 单色蓝 |
|
|
102
|
+
| `neon_cyberpunk` | Neon Cyberpunk | 霓虹赛博朋克 |
|
|
103
|
+
| `noir` | Noir | 纯黑白 |
|
|
104
|
+
| `nordic_frost` | Nordic Frost | 北欧冰霜 |
|
|
105
|
+
| `ocean` | Ocean | 海洋蓝 |
|
|
106
|
+
| `ocean_abyss` | Ocean Abyss | 深海深渊 |
|
|
107
|
+
| `pastel_dream` | Pastel Dream | 梦幻粉彩 |
|
|
108
|
+
| `qingzhuan` | 青砖 Qingzhuan | 胡同灰砖 |
|
|
109
|
+
| `red_alert` | Red Alert | 红色警报 |
|
|
110
|
+
| `royal_velvet` | Royal Velvet | 皇家天鹅绒 |
|
|
111
|
+
| `sakura_branch` | Sakura Branch | 樱花粉 |
|
|
112
|
+
| `shuimo` | 水墨 Shuimo | 水墨白底 |
|
|
113
|
+
| `solarized_dark` | Solarized Dark | Solarized 暗色 |
|
|
114
|
+
| `sulfur_slate` | Sulfur & Slate | 硫磺金灰 |
|
|
115
|
+
| `sunset` | Sunset | 落日橙粉 |
|
|
116
|
+
| `terra_clay` | Terra Clay | 陶土暖色 |
|
|
117
|
+
| `terracotta` | Terracotta | 地中海陶 |
|
|
118
|
+
| `vintage_nautical` | Vintage Nautical | 复古航海 |
|
|
119
|
+
| `warm_beige` | Warm Beige | 暖米色 |
|
|
120
|
+
|
|
121
|
+
## Credit
|
|
122
|
+
|
|
123
|
+
Forked from [maptoposter](https://github.com/originalankur/maptoposter) by Ankur Gupta. MIT License.
|
|
124
|
+
|
|
125
|
+
SinoGDB data derived from OpenStreetMap, processed by QGIS.
|
|
File without changes
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLI entry point for maptoposter.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import traceback
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cityposter.config import (
|
|
14
|
+
DEFAULT_THEME,
|
|
15
|
+
DEFAULT_DISTANCE_M,
|
|
16
|
+
DEFAULT_WIDTH_IN,
|
|
17
|
+
DEFAULT_HEIGHT_IN,
|
|
18
|
+
POSTERS_DIR,
|
|
19
|
+
CHINA_DEFAULT_DISTANCE_M,
|
|
20
|
+
)
|
|
21
|
+
from cityposter.themes.manager import load_theme, get_available_themes, list_themes
|
|
22
|
+
from cityposter.fonts.manager import load_fonts
|
|
23
|
+
from cityposter.rendering.poster import (
|
|
24
|
+
create_poster_osmnx,
|
|
25
|
+
create_poster_sinogdb,
|
|
26
|
+
generate_output_filename,
|
|
27
|
+
)
|
|
28
|
+
from cityposter.coordinate_systems import is_out_of_china, gcj02_to_wgs84, wgs84_to_gcj02
|
|
29
|
+
from cityposter.data.sinogdb import fetch_china_city_data, SinoGDBReader
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_coord(val: str) -> float:
|
|
33
|
+
"""Parse a coordinate value supporting DMS and decimal formats."""
|
|
34
|
+
try:
|
|
35
|
+
return float(val)
|
|
36
|
+
except ValueError:
|
|
37
|
+
from lat_lon_parser import parse
|
|
38
|
+
return parse(val)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
description="Generate beautiful map posters for any city",
|
|
44
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
45
|
+
epilog="""
|
|
46
|
+
Examples:
|
|
47
|
+
# Chinese city with SinoGDB (offline, fast)
|
|
48
|
+
maptoposter --lat 39.9042 --lon 116.4074 -dc "北京" -dC "中国" --font-family "Noto Sans SC"
|
|
49
|
+
|
|
50
|
+
# International city with OSMnx
|
|
51
|
+
maptoposter -c "Paris" -C "France" -t pastel_dream -d 10000
|
|
52
|
+
|
|
53
|
+
# GCJ-02 coordinates from Amap/Gaode
|
|
54
|
+
maptoposter --lat 39.9151 --lon 116.4040 --gcj02 -dc "北京" -dC "中国"
|
|
55
|
+
""",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Location
|
|
59
|
+
parser.add_argument("--city", "-c", type=str, help="City name")
|
|
60
|
+
parser.add_argument("--country", "-C", type=str, help="Country name")
|
|
61
|
+
parser.add_argument("--latitude", "-lat", type=str, help="Latitude (WGS-84 or GCJ-02 with --gcj02)")
|
|
62
|
+
parser.add_argument("--longitude", "-lon", type=str, help="Longitude (WGS-84 or GCJ-02 with --gcj02)")
|
|
63
|
+
parser.add_argument("--display-city", "-dc", type=str, help="Display name for city")
|
|
64
|
+
parser.add_argument("--display-country", "-dC", type=str, help="Display name for country")
|
|
65
|
+
parser.add_argument("--country-label", type=str, help="Override country text on poster")
|
|
66
|
+
|
|
67
|
+
# Data source
|
|
68
|
+
parser.add_argument("--data-source", choices=["sinogdb", "osmnx", "overpass"],
|
|
69
|
+
default=None,
|
|
70
|
+
help="Data source (sinogdb for China, osmnx/overpass for international)")
|
|
71
|
+
parser.add_argument("--sinogdb-dir", type=str,
|
|
72
|
+
help="SinoGDB data directory path")
|
|
73
|
+
|
|
74
|
+
# Theme
|
|
75
|
+
parser.add_argument("--theme", "-t", type=str, default=DEFAULT_THEME, help="Theme name")
|
|
76
|
+
parser.add_argument("--all-themes", action="store_true", help="Generate posters for all themes")
|
|
77
|
+
parser.add_argument("--list-themes", action="store_true", help="List all available themes")
|
|
78
|
+
|
|
79
|
+
# Map geometry
|
|
80
|
+
parser.add_argument("--distance", "-d", type=int, default=None,
|
|
81
|
+
help="Map radius in meters")
|
|
82
|
+
parser.add_argument("--width", "-W", type=float, default=DEFAULT_WIDTH_IN,
|
|
83
|
+
help="Image width in inches (max 20)")
|
|
84
|
+
parser.add_argument("--height", "-H", type=float, default=DEFAULT_HEIGHT_IN,
|
|
85
|
+
help="Image height in inches (max 20)")
|
|
86
|
+
|
|
87
|
+
# Format
|
|
88
|
+
parser.add_argument("--format", "-f", default="png", choices=["png", "svg", "pdf"],
|
|
89
|
+
help="Output format (default: png)")
|
|
90
|
+
|
|
91
|
+
# Fonts
|
|
92
|
+
parser.add_argument("--font-family", type=str,
|
|
93
|
+
help='Google Fonts family name (e.g., "Noto Sans SC")')
|
|
94
|
+
|
|
95
|
+
# China-specific
|
|
96
|
+
parser.add_argument("--gcj02", action="store_true",
|
|
97
|
+
help="Treat input coordinates as GCJ-02 (convert to WGS-84 for OSM data)")
|
|
98
|
+
|
|
99
|
+
# Road styling
|
|
100
|
+
parser.add_argument("--road-width-boost", type=float, default=None,
|
|
101
|
+
help="Multiplier for road line widths (default: 2.5 for sinogdb, 1.0 for osmnx)")
|
|
102
|
+
|
|
103
|
+
# POI
|
|
104
|
+
parser.add_argument("--poi", action="store_true", default=False,
|
|
105
|
+
help="Show all POI markers (landmarks, nature, schools, etc.). Default: subway + train only")
|
|
106
|
+
parser.add_argument("--custom-poi", nargs="?", dest="custom_pois", default=None,
|
|
107
|
+
const=[], metavar="NAME,LAT,LON,CATEGORY",
|
|
108
|
+
help="Load config/custom-poi/ POIs and optional CLI custom POI")
|
|
109
|
+
|
|
110
|
+
args = parser.parse_args()
|
|
111
|
+
|
|
112
|
+
# ── No args: show help ──────────────────────────────────
|
|
113
|
+
if len(sys.argv) == 1:
|
|
114
|
+
parser.print_help()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# ── List themes ─────────────────────────────────────────
|
|
118
|
+
if args.list_themes:
|
|
119
|
+
list_themes()
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# ── Validate location ───────────────────────────────────
|
|
123
|
+
if not args.latitude or not args.longitude:
|
|
124
|
+
if not args.city or not args.country:
|
|
125
|
+
print("Error: Provide either (--lat AND --lon) or (--city AND --country).\n")
|
|
126
|
+
parser.print_help()
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
# ── Parse coordinates ───────────────────────────────────
|
|
130
|
+
lat = parse_coord(args.latitude) if args.latitude else None
|
|
131
|
+
lon = parse_coord(args.longitude) if args.longitude else None
|
|
132
|
+
|
|
133
|
+
# GCJ-02 conversion
|
|
134
|
+
if args.gcj02 and lat is not None and lon is not None:
|
|
135
|
+
print(f"Input (GCJ-02): {lat:.6f}, {lon:.6f}")
|
|
136
|
+
lon, lat = gcj02_to_wgs84(lon, lat)
|
|
137
|
+
print(f"Converted (WGS-84): {lat:.6f}, {lon:.6f}")
|
|
138
|
+
|
|
139
|
+
# ── Auto-detect data source ─────────────────────────────
|
|
140
|
+
data_source = args.data_source
|
|
141
|
+
if data_source is None:
|
|
142
|
+
if lat is not None and lon is not None and not is_out_of_china(lon, lat):
|
|
143
|
+
data_source = "sinogdb"
|
|
144
|
+
print("✓ Auto-detected China location → using SinoGDB (offline)")
|
|
145
|
+
else:
|
|
146
|
+
data_source = "osmnx"
|
|
147
|
+
|
|
148
|
+
# ── Distance defaults ───────────────────────────────────
|
|
149
|
+
if args.distance is None:
|
|
150
|
+
if data_source == "sinogdb":
|
|
151
|
+
args.distance = CHINA_DEFAULT_DISTANCE_M
|
|
152
|
+
else:
|
|
153
|
+
args.distance = DEFAULT_DISTANCE_M
|
|
154
|
+
|
|
155
|
+
# ── Dimension limits ────────────────────────────────────
|
|
156
|
+
if args.width > 20:
|
|
157
|
+
print(f"⚠ Width {args.width} exceeds max (20). Using 20.")
|
|
158
|
+
args.width = 20.0
|
|
159
|
+
if args.height > 20:
|
|
160
|
+
print(f"⚠ Height {args.height} exceeds max (20). Using 20.")
|
|
161
|
+
args.height = 20.0
|
|
162
|
+
|
|
163
|
+
# ── Themes ──────────────────────────────────────────────
|
|
164
|
+
available_themes = get_available_themes()
|
|
165
|
+
if not available_themes:
|
|
166
|
+
print("No themes found in 'themes/' directory.")
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
if args.all_themes:
|
|
170
|
+
themes_to_generate = available_themes
|
|
171
|
+
else:
|
|
172
|
+
if args.theme not in available_themes:
|
|
173
|
+
print(f"Error: Theme '{args.theme}' not found.")
|
|
174
|
+
print(f"Available: {', '.join(available_themes)}")
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
themes_to_generate = [args.theme]
|
|
177
|
+
|
|
178
|
+
# ── Display names ───────────────────────────────────────
|
|
179
|
+
display_city = args.display_city or args.city or ""
|
|
180
|
+
display_country = args.display_country or args.country_label or args.country or ""
|
|
181
|
+
|
|
182
|
+
# Coordinates for display — use GCJ-02 if China
|
|
183
|
+
display_lat, display_lon = lat, lon
|
|
184
|
+
if data_source == "sinogdb" and not args.gcj02:
|
|
185
|
+
# Data is WGS-84; convert to GCJ-02 for display (matches Chinese user expectation)
|
|
186
|
+
display_lon, display_lat = wgs84_to_gcj02(lon, lat)
|
|
187
|
+
|
|
188
|
+
# ── Fonts ───────────────────────────────────────────────
|
|
189
|
+
# Priority: user-specified > LXGWNeoZhiSong + Fraunces > Noto Sans SC > Roboto
|
|
190
|
+
custom_fonts = None
|
|
191
|
+
if args.font_family:
|
|
192
|
+
custom_fonts = load_fonts(args.font_family)
|
|
193
|
+
|
|
194
|
+
if data_source == "sinogdb" and not custom_fonts:
|
|
195
|
+
# 1st: local LXGWNeoZhiSong (CJK serif) + Fraunces (Latin serif)
|
|
196
|
+
lxgw = Path("fonts/LXGWNeoZhiSong.ttf")
|
|
197
|
+
fraunces = Path("fonts/Fraunces_72pt-Regular.ttf")
|
|
198
|
+
if lxgw.exists():
|
|
199
|
+
custom_fonts = {
|
|
200
|
+
"bold": str(lxgw), # LXGW covers both CJK + Latin
|
|
201
|
+
"regular": str(lxgw),
|
|
202
|
+
"light": str(lxgw),
|
|
203
|
+
}
|
|
204
|
+
print(f"✓ Using LXGW Neo ZhiSong + Fraunces")
|
|
205
|
+
else:
|
|
206
|
+
# 2nd: Google Fonts Noto Sans SC
|
|
207
|
+
print("Auto-loading Chinese font...")
|
|
208
|
+
custom_fonts = load_fonts("Noto Sans SC")
|
|
209
|
+
if not custom_fonts:
|
|
210
|
+
# 3rd: local cache
|
|
211
|
+
cached = Path("fonts/cache")
|
|
212
|
+
local = {
|
|
213
|
+
"bold": str(cached / "noto_sans_sc_bold.ttf"),
|
|
214
|
+
"regular": str(cached / "noto_sans_sc_regular.ttf"),
|
|
215
|
+
"light": str(cached / "noto_sans_sc_light.ttf"),
|
|
216
|
+
}
|
|
217
|
+
if all(Path(p).exists() for p in local.values()):
|
|
218
|
+
custom_fonts = local
|
|
219
|
+
print("✓ Using cached Noto Sans SC")
|
|
220
|
+
|
|
221
|
+
# ── Generate ────────────────────────────────────────────
|
|
222
|
+
print("=" * 50)
|
|
223
|
+
print("City Map Poster Generator")
|
|
224
|
+
print(f"Data source: {data_source}")
|
|
225
|
+
print("=" * 50)
|
|
226
|
+
|
|
227
|
+
# Resolve city/country names
|
|
228
|
+
city_name = args.city or args.display_city or f"{lat:.4f},{lon:.4f}"
|
|
229
|
+
country_name = args.country or args.display_country or ""
|
|
230
|
+
|
|
231
|
+
# Coordinates
|
|
232
|
+
if lat is None or lon is None:
|
|
233
|
+
# Need geocoding (OSMnx path)
|
|
234
|
+
from cityposter.geocoding import get_coordinates
|
|
235
|
+
lat, lon = get_coordinates(args.city, args.country)
|
|
236
|
+
display_lat, display_lon = lat, lon
|
|
237
|
+
|
|
238
|
+
point = (lat, lon)
|
|
239
|
+
display_point = (display_lat, display_lon)
|
|
240
|
+
|
|
241
|
+
# Build reproduction shell command (will be updated per theme)
|
|
242
|
+
shell_cmd_base = "cityposter " + " ".join(
|
|
243
|
+
f'"{a}"' if " " in a else a for a in sys.argv[1:]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
for theme_name in themes_to_generate:
|
|
248
|
+
theme = load_theme(theme_name)
|
|
249
|
+
output_file = generate_output_filename(city_name, theme_name, args.format)
|
|
250
|
+
|
|
251
|
+
if data_source == "sinogdb":
|
|
252
|
+
print(f"\nGenerating SinoGDB poster for {display_city or city_name}...")
|
|
253
|
+
sino_data = fetch_china_city_data(
|
|
254
|
+
lat, lon, args.distance,
|
|
255
|
+
data_dir=args.sinogdb_dir,
|
|
256
|
+
include_pois=True, # always fetch railway/transport for custom POIs too
|
|
257
|
+
include_buildings=False,
|
|
258
|
+
)
|
|
259
|
+
# Inject custom POIs (only when --custom-poi flag is used)
|
|
260
|
+
if args.custom_pois is not None:
|
|
261
|
+
from geopandas import GeoDataFrame
|
|
262
|
+
from shapely.geometry import Point as ShapelyPoint
|
|
263
|
+
|
|
264
|
+
# 1. CLI inline custom-poi value
|
|
265
|
+
if args.custom_pois: # non-empty list
|
|
266
|
+
spec = args.custom_pois if isinstance(args.custom_pois, str) else args.custom_pois[0]
|
|
267
|
+
parts = spec.split(",")
|
|
268
|
+
if len(parts) >= 4:
|
|
269
|
+
name, clat, clon, cat = parts[0], float(parts[1]), float(parts[2]), parts[3]
|
|
270
|
+
cli_gdf = GeoDataFrame(
|
|
271
|
+
[{"name": name, "station": cat, "geometry": ShapelyPoint(clon, clat)}],
|
|
272
|
+
crs="EPSG:4326"
|
|
273
|
+
)
|
|
274
|
+
if sino_data.get("railway_stations") is not None:
|
|
275
|
+
sino_data["railway_stations"] = GeoDataFrame(
|
|
276
|
+
list(sino_data["railway_stations"].to_dict("records")) +
|
|
277
|
+
cli_gdf.to_dict("records"), crs="EPSG:4326"
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
sino_data["railway_stations"] = cli_gdf
|
|
281
|
+
print(f"✓ Custom POI: {name}")
|
|
282
|
+
|
|
283
|
+
# 2. Config file POIs
|
|
284
|
+
from cityposter.config_poi import load_custom_pois
|
|
285
|
+
config_gdf = load_custom_pois(city=display_city)
|
|
286
|
+
if not config_gdf.empty:
|
|
287
|
+
config_records = config_gdf.to_dict("records")
|
|
288
|
+
if sino_data.get("railway_stations") is not None and not sino_data["railway_stations"].empty:
|
|
289
|
+
sino_data["railway_stations"] = GeoDataFrame(
|
|
290
|
+
list(sino_data["railway_stations"].to_dict("records")) + config_records,
|
|
291
|
+
crs="EPSG:4326"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
sino_data["railway_stations"] = config_gdf
|
|
295
|
+
print(f"✓ Custom POI: {len(config_gdf)} from config/")
|
|
296
|
+
# Default road width boost for SinoGDB: thicker lines compensate for
|
|
297
|
+
# the fact that SinoGDB roads are individual line segments (vs OSMnx graph edges)
|
|
298
|
+
road_boost = args.road_width_boost if args.road_width_boost is not None else 2.5
|
|
299
|
+
create_poster_sinogdb(
|
|
300
|
+
city=display_city,
|
|
301
|
+
country=display_country,
|
|
302
|
+
sino_data=sino_data,
|
|
303
|
+
point=display_point,
|
|
304
|
+
dist=args.distance,
|
|
305
|
+
theme=theme,
|
|
306
|
+
output_file=output_file,
|
|
307
|
+
output_format=args.format,
|
|
308
|
+
width=args.width,
|
|
309
|
+
height=args.height,
|
|
310
|
+
display_city=display_city,
|
|
311
|
+
display_country=display_country,
|
|
312
|
+
fonts=custom_fonts,
|
|
313
|
+
road_width_boost=road_boost,
|
|
314
|
+
include_pois=args.poi,
|
|
315
|
+
shell_cmd=shell_cmd_base,
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
# OSMnx path (existing)
|
|
319
|
+
from cityposter.data.fetcher import fetch_osmnx_data
|
|
320
|
+
print(f"\nGenerating OSMnx poster for {display_city or city_name}...")
|
|
321
|
+
g, water, parks = fetch_osmnx_data(point, args.distance, args.width, args.height)
|
|
322
|
+
|
|
323
|
+
create_poster_osmnx(
|
|
324
|
+
city=display_city,
|
|
325
|
+
country=display_country,
|
|
326
|
+
g=g,
|
|
327
|
+
water=water,
|
|
328
|
+
parks=parks,
|
|
329
|
+
point=display_point,
|
|
330
|
+
dist=args.distance,
|
|
331
|
+
theme=theme,
|
|
332
|
+
output_file=output_file,
|
|
333
|
+
output_format=args.format,
|
|
334
|
+
width=args.width,
|
|
335
|
+
height=args.height,
|
|
336
|
+
display_city=display_city,
|
|
337
|
+
display_country=display_country,
|
|
338
|
+
fonts=custom_fonts,
|
|
339
|
+
shell_cmd=shell_cmd_base,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
print("\n" + "=" * 50)
|
|
343
|
+
print("✓ Poster generation complete!")
|
|
344
|
+
print("=" * 50)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print(f"\n✗ Error: {e}")
|
|
348
|
+
traceback.print_exc()
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
main()
|